craft-native 0.0.37 → 0.0.49

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.
Files changed (89) hide show
  1. package/dist/android/src/index.js +268 -0
  2. package/dist/android/src/index.js.map +10 -0
  3. package/dist/android/templates/AndroidManifest.xml.template +69 -0
  4. package/dist/android/templates/CraftBridge.kt.template +3748 -0
  5. package/dist/android/templates/CraftBridgeExtensions.kt.template +383 -0
  6. package/dist/android/templates/CraftWidgetProvider.kt.template +246 -0
  7. package/dist/android/templates/MainActivity.kt.template +109 -0
  8. package/dist/android/templates/build.gradle.kts.app.template +80 -0
  9. package/dist/android/templates/build.gradle.kts.project.template +5 -0
  10. package/dist/android/templates/fastlane/Appfile +25 -0
  11. package/dist/android/templates/fastlane/Fastfile +192 -0
  12. package/dist/android/templates/fastlane/Gemfile +7 -0
  13. package/dist/android/templates/github-workflow-android.yml +248 -0
  14. package/dist/android/templates/github-workflow-test.yml +329 -0
  15. package/dist/android/templates/settings.gradle.kts.template +18 -0
  16. package/dist/android/templates/test-bridges.html +1463 -0
  17. package/dist/api/window.d.ts +18 -0
  18. package/dist/api/window.d.ts.map +1 -1
  19. package/dist/assets/index.d.ts.map +1 -1
  20. package/dist/bridge/core.d.ts +2 -0
  21. package/dist/bridge/core.d.ts.map +1 -1
  22. package/dist/cli.js +4168 -0
  23. package/dist/components/sidebar.d.ts +5 -1
  24. package/dist/components/sidebar.d.ts.map +1 -1
  25. package/dist/dev/devtools.d.ts +1 -1
  26. package/dist/dev/devtools.d.ts.map +1 -1
  27. package/dist/dev/hot-reload.d.ts +12 -3
  28. package/dist/dev/hot-reload.d.ts.map +1 -1
  29. package/dist/index.cjs +573 -208
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +585 -216
  32. package/dist/index.js.map +45 -0
  33. package/dist/ios/src/index.js +186 -0
  34. package/dist/ios/src/index.js.map +10 -0
  35. package/dist/ios/templates/CraftApp.swift +4241 -0
  36. package/dist/ios/templates/CraftAppExtensions.swift +361 -0
  37. package/dist/ios/templates/CraftWidget.swift +247 -0
  38. package/dist/ios/templates/Info.plist.template +65 -0
  39. package/dist/ios/templates/fastlane/Appfile +45 -0
  40. package/dist/ios/templates/fastlane/Fastfile +189 -0
  41. package/dist/ios/templates/fastlane/Gemfile +7 -0
  42. package/dist/ios/templates/fastlane/Matchfile +58 -0
  43. package/dist/ios/templates/github-workflow-ios.yml +192 -0
  44. package/dist/ios/templates/github-workflow-release.yml +175 -0
  45. package/dist/ios/templates/github-workflow-test.yml +272 -0
  46. package/dist/ios/templates/project.yml.template +25 -0
  47. package/dist/ios/templates/test-bridges.html +1463 -0
  48. package/dist/package.d.ts +106 -2
  49. package/dist/package.d.ts.map +1 -1
  50. package/dist/package.test.d.ts +2 -0
  51. package/dist/package.test.d.ts.map +1 -0
  52. package/dist/styles/sidebar-templates.d.ts.map +1 -1
  53. package/dist/styles/sidebars.d.ts +12 -8
  54. package/dist/styles/sidebars.d.ts.map +1 -1
  55. package/dist/templates/projects/blank/craft.config.ts +19 -0
  56. package/dist/templates/projects/blank/index.html +27 -0
  57. package/dist/templates/projects/blank/package.json +19 -0
  58. package/dist/templates/projects/blank/src/main.ts +26 -0
  59. package/dist/templates/projects/dashboard/craft.config.ts +42 -0
  60. package/dist/templates/projects/dashboard/index.html +13 -0
  61. package/dist/templates/projects/dashboard/package.json +20 -0
  62. package/dist/templates/projects/dashboard/src/main.ts +163 -0
  63. package/dist/templates/projects/dashboard/src/styles.css +280 -0
  64. package/dist/templates/projects/drawer/craft.config.ts +47 -0
  65. package/dist/templates/projects/drawer/index.html +13 -0
  66. package/dist/templates/projects/drawer/package.json +21 -0
  67. package/dist/templates/projects/drawer/src/main.ts +406 -0
  68. package/dist/templates/projects/drawer/src/styles.css +468 -0
  69. package/dist/templates/projects/ecommerce/craft.config.ts +32 -0
  70. package/dist/templates/projects/ecommerce/index.html +13 -0
  71. package/dist/templates/projects/ecommerce/package.json +19 -0
  72. package/dist/templates/projects/ecommerce/src/main.ts +194 -0
  73. package/dist/templates/projects/ecommerce/src/styles.css +169 -0
  74. package/dist/templates/projects/social/craft.config.ts +32 -0
  75. package/dist/templates/projects/social/index.html +13 -0
  76. package/dist/templates/projects/social/package.json +19 -0
  77. package/dist/templates/projects/social/src/main.ts +270 -0
  78. package/dist/templates/projects/social/src/styles.css +216 -0
  79. package/dist/templates/projects/tabs/craft.config.ts +47 -0
  80. package/dist/templates/projects/tabs/index.html +13 -0
  81. package/dist/templates/projects/tabs/package.json +21 -0
  82. package/dist/templates/projects/tabs/src/main.ts +177 -0
  83. package/dist/templates/projects/tabs/src/styles.css +163 -0
  84. package/dist/types.d.ts +29 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/updater/index.d.ts +1 -0
  87. package/dist/updater/index.d.ts.map +1 -1
  88. package/package.json +5 -12
  89. package/bin/cli.ts +0 -937
@@ -0,0 +1,3748 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.ClipData
6
+ import android.content.ClipboardManager
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.pm.PackageManager
10
+ import android.graphics.Bitmap
11
+ import android.hardware.camera2.CameraAccessException
12
+ import android.hardware.camera2.CameraCharacteristics
13
+ import android.hardware.camera2.CameraManager
14
+ import android.location.Location
15
+ import android.net.ConnectivityManager
16
+ import android.net.Network
17
+ import android.net.NetworkCapabilities
18
+ import android.net.NetworkRequest
19
+ import android.net.Uri
20
+ import android.os.Build
21
+ import android.os.Bundle
22
+ import android.os.Looper
23
+ import android.os.VibrationEffect
24
+ import android.os.Vibrator
25
+ import android.os.VibratorManager
26
+ import android.provider.MediaStore
27
+ import android.provider.Settings
28
+ import android.speech.RecognitionListener
29
+ import android.speech.RecognizerIntent
30
+ import android.speech.SpeechRecognizer
31
+ import android.util.DisplayMetrics
32
+ import android.webkit.JavascriptInterface
33
+ import android.webkit.WebView
34
+ import androidx.biometric.BiometricManager
35
+ import androidx.biometric.BiometricPrompt
36
+ import androidx.core.app.ActivityCompat
37
+ import androidx.core.app.NotificationManagerCompat
38
+ import androidx.core.content.ContextCompat
39
+ import androidx.fragment.app.FragmentActivity
40
+ import androidx.lifecycle.Lifecycle
41
+ import androidx.lifecycle.LifecycleEventObserver
42
+ import androidx.lifecycle.ProcessLifecycleOwner
43
+ import androidx.security.crypto.EncryptedSharedPreferences
44
+ import androidx.security.crypto.MasterKey
45
+ import com.google.android.gms.location.FusedLocationProviderClient
46
+ import com.google.android.gms.location.LocationCallback
47
+ import com.google.android.gms.location.LocationRequest
48
+ import com.google.android.gms.location.LocationResult
49
+ import com.google.android.gms.location.LocationServices
50
+ import com.google.android.gms.location.Priority
51
+ import com.google.android.play.core.review.ReviewManagerFactory
52
+ import android.content.ContentResolver
53
+ import android.content.ContentUris
54
+ import android.content.ContentValues
55
+ import android.database.Cursor
56
+ import android.provider.CalendarContract
57
+ import android.provider.ContactsContract
58
+ import android.app.NotificationChannel
59
+ import android.app.NotificationManager
60
+ import android.app.PendingIntent
61
+ import android.view.WindowManager
62
+ import android.content.pm.ActivityInfo
63
+ import com.android.billingclient.api.*
64
+ import org.json.JSONObject
65
+ import org.json.JSONArray
66
+ import java.util.Locale
67
+ import java.util.concurrent.Executor
68
+ import java.util.Calendar
69
+ import java.util.TimeZone
70
+ import android.media.MediaRecorder
71
+ import android.hardware.Sensor
72
+ import android.hardware.SensorEvent
73
+ import android.hardware.SensorEventListener
74
+ import android.hardware.SensorManager
75
+ import android.database.sqlite.SQLiteDatabase
76
+ import android.bluetooth.BluetoothAdapter
77
+ import android.bluetooth.BluetoothManager
78
+ import android.bluetooth.le.BluetoothLeScanner
79
+ import android.bluetooth.le.ScanCallback
80
+ import android.bluetooth.le.ScanResult
81
+ import android.nfc.NfcAdapter
82
+ import android.nfc.tech.Ndef
83
+ import android.app.DownloadManager
84
+ import android.view.View
85
+ import android.util.Base64
86
+ import com.google.mlkit.vision.barcode.BarcodeScanning
87
+ import com.google.mlkit.vision.common.InputImage
88
+ import com.google.mlkit.vision.label.ImageLabeling
89
+ import com.google.mlkit.vision.label.defaults.ImageLabelerOptions
90
+ import com.google.mlkit.vision.objects.ObjectDetection
91
+ import com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions
92
+ import com.google.mlkit.vision.text.TextRecognition
93
+ import com.google.mlkit.vision.text.latin.TextRecognizerOptions
94
+ import android.graphics.BitmapFactory
95
+ import java.io.ByteArrayOutputStream
96
+ import java.io.File
97
+ import java.util.concurrent.TimeUnit
98
+
99
+ class CraftBridge(
100
+ private val activity: Activity,
101
+ private val webView: WebView
102
+ ) {
103
+ private var speechRecognizer: SpeechRecognizer? = null
104
+ private var isListening = false
105
+ private val mainExecutor: Executor = ContextCompat.getMainExecutor(activity)
106
+
107
+ // Location tracking
108
+ private var fusedLocationClient: FusedLocationProviderClient? = null
109
+ private var locationCallback: LocationCallback? = null
110
+ private var watchId = 0
111
+
112
+ // Network monitoring
113
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
114
+
115
+ // Flashlight state
116
+ private var isFlashlightOn = false
117
+
118
+ // App state tracking
119
+ private var currentAppState = "active"
120
+
121
+ // Keep awake
122
+ private var isKeepingAwake = false
123
+
124
+ // Billing client for in-app purchases
125
+ private var billingClient: BillingClient? = null
126
+
127
+ // Notification channel ID
128
+ private val notificationChannelId = "craft_notifications"
129
+
130
+ // Audio recording
131
+ private var mediaRecorder: MediaRecorder? = null
132
+ private var audioFile: File? = null
133
+
134
+ // Motion sensors
135
+ private var sensorManager: SensorManager? = null
136
+ private var accelerometer: Sensor? = null
137
+ private var gyroscope: Sensor? = null
138
+ private var sensorListener: SensorEventListener? = null
139
+
140
+ // SQLite database
141
+ private var database: SQLiteDatabase? = null
142
+
143
+ // Bluetooth
144
+ private var bluetoothAdapter: BluetoothAdapter? = null
145
+ private var bluetoothScanner: BluetoothLeScanner? = null
146
+ private var bleScanCallback: ScanCallback? = null
147
+
148
+ // Encrypted shared preferences for secure storage
149
+ private val masterKey by lazy {
150
+ MasterKey.Builder(activity)
151
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
152
+ .build()
153
+ }
154
+
155
+ private val securePrefs by lazy {
156
+ EncryptedSharedPreferences.create(
157
+ activity,
158
+ "craft_secure_prefs",
159
+ masterKey,
160
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
161
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
162
+ )
163
+ }
164
+
165
+ fun injectBridge() {
166
+ val script = """
167
+ window.craft = {
168
+ platform: 'android',
169
+ capabilities: {
170
+ haptics: true,
171
+ speechRecognition: ${SpeechRecognizer.isRecognitionAvailable(activity)},
172
+ share: true,
173
+ camera: true,
174
+ biometric: ${isBiometricAvailable()},
175
+ pushNotifications: true,
176
+ secureStorage: true,
177
+ geolocation: true,
178
+ clipboard: true,
179
+ deviceInfo: true,
180
+ appBadge: true,
181
+ networkStatus: true,
182
+ appReview: true,
183
+ flashlight: ${hasFlashlight()},
184
+ openURL: true,
185
+ vibrationPattern: true,
186
+ appState: true,
187
+ contacts: true,
188
+ calendar: true,
189
+ localNotifications: true,
190
+ inAppPurchase: true,
191
+ keepAwake: true,
192
+ orientationLock: true,
193
+ deepLinks: true,
194
+ qrScanner: true,
195
+ filePicker: true,
196
+ fileDownload: true,
197
+ googleSignIn: true,
198
+ audioRecording: true,
199
+ videoRecording: true,
200
+ motionSensors: true,
201
+ localDatabase: true,
202
+ bluetooth: true,
203
+ nfc: ${NfcAdapter.getDefaultAdapter(activity) != null},
204
+ fitness: true,
205
+ screenCapture: true
206
+ },
207
+
208
+ // Haptic feedback
209
+ haptic: function(style) {
210
+ CraftAndroid.haptic(style || 'medium');
211
+ },
212
+
213
+ // Speech recognition
214
+ startListening: function() {
215
+ CraftAndroid.startListening();
216
+ },
217
+ stopListening: function() {
218
+ CraftAndroid.stopListening();
219
+ },
220
+
221
+ // Share
222
+ share: function(text, title) {
223
+ CraftAndroid.share(text, title || '');
224
+ },
225
+
226
+ // Camera
227
+ openCamera: function() {
228
+ return new Promise(function(resolve, reject) {
229
+ window._craftCameraResolve = resolve;
230
+ window._craftCameraReject = reject;
231
+ CraftAndroid.openCamera();
232
+ });
233
+ },
234
+ pickImage: function() {
235
+ return new Promise(function(resolve, reject) {
236
+ window._craftGalleryResolve = resolve;
237
+ window._craftGalleryReject = reject;
238
+ CraftAndroid.pickImage();
239
+ });
240
+ },
241
+
242
+ // Biometric auth
243
+ authenticate: function(reason) {
244
+ return new Promise(function(resolve, reject) {
245
+ window._craftBiometricResolve = resolve;
246
+ window._craftBiometricReject = reject;
247
+ CraftAndroid.authenticate(reason || 'Authenticate to continue');
248
+ });
249
+ },
250
+
251
+ // Push notifications
252
+ registerPush: function() {
253
+ return new Promise(function(resolve, reject) {
254
+ window._craftPushResolve = resolve;
255
+ window._craftPushReject = reject;
256
+ CraftAndroid.registerPush();
257
+ });
258
+ },
259
+
260
+ // Secure storage
261
+ secureStore: {
262
+ set: function(key, value) {
263
+ return CraftAndroid.secureSet(key, value);
264
+ },
265
+ get: function(key) {
266
+ return CraftAndroid.secureGet(key);
267
+ },
268
+ remove: function(key) {
269
+ return CraftAndroid.secureRemove(key);
270
+ }
271
+ },
272
+
273
+ // Geolocation
274
+ getCurrentPosition: function(options) {
275
+ return new Promise(function(resolve, reject) {
276
+ window._craftLocationResolve = resolve;
277
+ window._craftLocationReject = reject;
278
+ CraftAndroid.getCurrentPosition(JSON.stringify(options || {}));
279
+ });
280
+ },
281
+ watchPosition: function(callback, options) {
282
+ var watchId = CraftAndroid.watchPosition(JSON.stringify(options || {}));
283
+ window['_craftLocationWatch_' + watchId] = callback;
284
+ return watchId;
285
+ },
286
+ clearWatch: function(watchId) {
287
+ CraftAndroid.clearWatch(watchId);
288
+ delete window['_craftLocationWatch_' + watchId];
289
+ },
290
+
291
+ // Clipboard
292
+ clipboard: {
293
+ read: function() {
294
+ return new Promise(function(resolve) {
295
+ resolve(CraftAndroid.clipboardRead());
296
+ });
297
+ },
298
+ write: function(text) {
299
+ return new Promise(function(resolve) {
300
+ resolve(CraftAndroid.clipboardWrite(text));
301
+ });
302
+ }
303
+ },
304
+
305
+ // Device Info
306
+ getDeviceInfo: function() {
307
+ return JSON.parse(CraftAndroid.getDeviceInfo());
308
+ },
309
+
310
+ // App Badge
311
+ setBadge: function(count) {
312
+ CraftAndroid.setBadge(count);
313
+ },
314
+ clearBadge: function() {
315
+ CraftAndroid.clearBadge();
316
+ },
317
+
318
+ // Network Status
319
+ getNetworkStatus: function() {
320
+ return JSON.parse(CraftAndroid.getNetworkStatus());
321
+ },
322
+ onNetworkChange: function(callback) {
323
+ window._craftNetworkChangeCallback = callback;
324
+ CraftAndroid.startNetworkMonitoring();
325
+ },
326
+ offNetworkChange: function() {
327
+ window._craftNetworkChangeCallback = null;
328
+ CraftAndroid.stopNetworkMonitoring();
329
+ },
330
+
331
+ // App Review
332
+ requestReview: function() {
333
+ return new Promise(function(resolve, reject) {
334
+ window._craftReviewResolve = resolve;
335
+ window._craftReviewReject = reject;
336
+ CraftAndroid.requestReview();
337
+ });
338
+ },
339
+
340
+ // Flashlight
341
+ setFlashlight: function(on) {
342
+ return CraftAndroid.setFlashlight(on);
343
+ },
344
+ toggleFlashlight: function() {
345
+ return CraftAndroid.toggleFlashlight();
346
+ },
347
+
348
+ // Open URL
349
+ openURL: function(url) {
350
+ return CraftAndroid.openURL(url);
351
+ },
352
+
353
+ // Vibration Pattern
354
+ vibrate: function(pattern) {
355
+ CraftAndroid.vibrate(JSON.stringify(pattern));
356
+ },
357
+
358
+ // App State
359
+ getAppState: function() {
360
+ return CraftAndroid.getAppState();
361
+ },
362
+ onAppStateChange: function(callback) {
363
+ window._craftAppStateCallback = callback;
364
+ CraftAndroid.startAppStateMonitoring();
365
+ },
366
+ offAppStateChange: function() {
367
+ window._craftAppStateCallback = null;
368
+ CraftAndroid.stopAppStateMonitoring();
369
+ },
370
+
371
+ // Contacts
372
+ contacts: {
373
+ getAll: function() {
374
+ return new Promise(function(resolve, reject) {
375
+ window._craftContactsResolve = resolve;
376
+ window._craftContactsReject = reject;
377
+ CraftAndroid.getContacts();
378
+ });
379
+ },
380
+ add: function(contact) {
381
+ return new Promise(function(resolve, reject) {
382
+ window._craftAddContactResolve = resolve;
383
+ window._craftAddContactReject = reject;
384
+ CraftAndroid.addContact(JSON.stringify(contact));
385
+ });
386
+ }
387
+ },
388
+
389
+ // Calendar
390
+ calendar: {
391
+ getEvents: function(startDate, endDate) {
392
+ return new Promise(function(resolve, reject) {
393
+ window._craftCalendarResolve = resolve;
394
+ window._craftCalendarReject = reject;
395
+ CraftAndroid.getCalendarEvents(startDate || 0, endDate || 0);
396
+ });
397
+ },
398
+ createEvent: function(event) {
399
+ return new Promise(function(resolve, reject) {
400
+ window._craftCreateEventResolve = resolve;
401
+ window._craftCreateEventReject = reject;
402
+ CraftAndroid.createCalendarEvent(JSON.stringify(event));
403
+ });
404
+ },
405
+ deleteEvent: function(eventId) {
406
+ return new Promise(function(resolve, reject) {
407
+ window._craftDeleteEventResolve = resolve;
408
+ window._craftDeleteEventReject = reject;
409
+ CraftAndroid.deleteCalendarEvent(eventId);
410
+ });
411
+ }
412
+ },
413
+
414
+ // Local Notifications
415
+ notifications: {
416
+ schedule: function(notification) {
417
+ return new Promise(function(resolve, reject) {
418
+ window._craftNotifResolve = resolve;
419
+ window._craftNotifReject = reject;
420
+ CraftAndroid.scheduleNotification(JSON.stringify(notification));
421
+ });
422
+ },
423
+ cancel: function(id) {
424
+ CraftAndroid.cancelNotification(id);
425
+ },
426
+ cancelAll: function() {
427
+ CraftAndroid.cancelAllNotifications();
428
+ }
429
+ },
430
+
431
+ // In-App Purchase
432
+ iap: {
433
+ getProducts: function(productIds) {
434
+ return new Promise(function(resolve, reject) {
435
+ window._craftProductsResolve = resolve;
436
+ window._craftProductsReject = reject;
437
+ CraftAndroid.getProducts(JSON.stringify(productIds));
438
+ });
439
+ },
440
+ purchase: function(productId) {
441
+ return new Promise(function(resolve, reject) {
442
+ window._craftPurchaseResolve = resolve;
443
+ window._craftPurchaseReject = reject;
444
+ CraftAndroid.purchase(productId);
445
+ });
446
+ },
447
+ restore: function() {
448
+ return new Promise(function(resolve, reject) {
449
+ window._craftRestoreResolve = resolve;
450
+ window._craftRestoreReject = reject;
451
+ CraftAndroid.restorePurchases();
452
+ });
453
+ }
454
+ },
455
+
456
+ // Keep Awake
457
+ setKeepAwake: function(enabled) {
458
+ return CraftAndroid.setKeepAwake(enabled);
459
+ },
460
+
461
+ // Orientation Lock
462
+ lockOrientation: function(orientation) {
463
+ return CraftAndroid.lockOrientation(orientation);
464
+ },
465
+ unlockOrientation: function() {
466
+ return CraftAndroid.unlockOrientation();
467
+ },
468
+
469
+ // Deep Links
470
+ onDeepLink: function(callback) {
471
+ window.addEventListener('craftDeepLink', function(e) { callback(e.detail); });
472
+ },
473
+
474
+ // QR/Barcode Scanner
475
+ scanQRCode: function() {
476
+ return new Promise(function(resolve, reject) {
477
+ window._craftQRResolve = resolve;
478
+ window._craftQRReject = reject;
479
+ CraftAndroid.scanQRCode();
480
+ });
481
+ },
482
+
483
+ // File Picker
484
+ pickFile: function(types) {
485
+ return new Promise(function(resolve, reject) {
486
+ window._craftFileResolve = resolve;
487
+ window._craftFileReject = reject;
488
+ CraftAndroid.pickFile(JSON.stringify(types || []));
489
+ });
490
+ },
491
+
492
+ // File Download
493
+ downloadFile: function(url, filename) {
494
+ return new Promise(function(resolve, reject) {
495
+ window._craftDownloadResolve = resolve;
496
+ window._craftDownloadReject = reject;
497
+ CraftAndroid.downloadFile(url, filename);
498
+ });
499
+ },
500
+ saveFile: function(data, filename) {
501
+ return new Promise(function(resolve, reject) {
502
+ window._craftSaveResolve = resolve;
503
+ window._craftSaveReject = reject;
504
+ CraftAndroid.saveFile(data, filename);
505
+ });
506
+ },
507
+
508
+ // Google Sign In
509
+ signInWithGoogle: function() {
510
+ return new Promise(function(resolve, reject) {
511
+ window._craftGoogleResolve = resolve;
512
+ window._craftGoogleReject = reject;
513
+ CraftAndroid.signInWithGoogle();
514
+ });
515
+ },
516
+
517
+ // Audio Recording
518
+ startAudioRecording: function() {
519
+ return new Promise(function(resolve, reject) {
520
+ window._craftAudioResolve = resolve;
521
+ window._craftAudioReject = reject;
522
+ CraftAndroid.startAudioRecording();
523
+ });
524
+ },
525
+ stopAudioRecording: function() {
526
+ return new Promise(function(resolve, reject) {
527
+ window._craftAudioStopResolve = resolve;
528
+ window._craftAudioStopReject = reject;
529
+ CraftAndroid.stopAudioRecording();
530
+ });
531
+ },
532
+
533
+ // Video Recording
534
+ startVideoRecording: function() {
535
+ return new Promise(function(resolve, reject) {
536
+ window._craftVideoResolve = resolve;
537
+ window._craftVideoReject = reject;
538
+ CraftAndroid.startVideoRecording();
539
+ });
540
+ },
541
+
542
+ // Motion Sensors
543
+ startMotionUpdates: function(interval) {
544
+ return CraftAndroid.startMotionUpdates(interval || 100);
545
+ },
546
+ stopMotionUpdates: function() {
547
+ CraftAndroid.stopMotionUpdates();
548
+ },
549
+ onMotionUpdate: function(callback) {
550
+ window.addEventListener('craftMotionUpdate', function(e) { callback(e.detail); });
551
+ },
552
+
553
+ // Local Database
554
+ db: {
555
+ execute: function(sql, params) {
556
+ return new Promise(function(resolve, reject) {
557
+ window._craftDbExecResolve = resolve;
558
+ window._craftDbExecReject = reject;
559
+ CraftAndroid.dbExecute(sql, JSON.stringify(params || []));
560
+ });
561
+ },
562
+ query: function(sql, params) {
563
+ return new Promise(function(resolve, reject) {
564
+ window._craftDbQueryResolve = resolve;
565
+ window._craftDbQueryReject = reject;
566
+ CraftAndroid.dbQuery(sql, JSON.stringify(params || []));
567
+ });
568
+ }
569
+ },
570
+
571
+ // Bluetooth
572
+ bluetooth: {
573
+ startScan: function() {
574
+ return new Promise(function(resolve, reject) {
575
+ window._craftBleResolve = resolve;
576
+ window._craftBleReject = reject;
577
+ CraftAndroid.startBluetoothScan();
578
+ });
579
+ },
580
+ stopScan: function() {
581
+ CraftAndroid.stopBluetoothScan();
582
+ },
583
+ onDevice: function(callback) {
584
+ window.addEventListener('craftBluetoothDevice', function(e) { callback(e.detail); });
585
+ }
586
+ },
587
+
588
+ // NFC
589
+ scanNFC: function() {
590
+ return new Promise(function(resolve, reject) {
591
+ window._craftNfcResolve = resolve;
592
+ window._craftNfcReject = reject;
593
+ CraftAndroid.scanNFC();
594
+ });
595
+ },
596
+
597
+ // Fitness / Health
598
+ fitness: {
599
+ requestAuthorization: function(types) {
600
+ return new Promise(function(resolve, reject) {
601
+ window._craftFitnessAuthResolve = resolve;
602
+ window._craftFitnessAuthReject = reject;
603
+ CraftAndroid.requestFitnessAuthorization(JSON.stringify(types || []));
604
+ });
605
+ },
606
+ getData: function(type, startDate, endDate) {
607
+ return new Promise(function(resolve, reject) {
608
+ window._craftFitnessDataResolve = resolve;
609
+ window._craftFitnessDataReject = reject;
610
+ CraftAndroid.getFitnessData(type, startDate || 0, endDate || 0);
611
+ });
612
+ }
613
+ },
614
+
615
+ // Screen Capture
616
+ takeScreenshot: function() {
617
+ return new Promise(function(resolve, reject) {
618
+ window._craftScreenshotResolve = resolve;
619
+ window._craftScreenshotReject = reject;
620
+ CraftAndroid.takeScreenshot();
621
+ });
622
+ },
623
+
624
+ // Background Tasks
625
+ backgroundTask: {
626
+ register: function(taskId) {
627
+ return new Promise(function(resolve, reject) {
628
+ window._craftBgTaskResolve = resolve;
629
+ window._craftBgTaskReject = reject;
630
+ CraftAndroid.registerBackgroundTask(taskId);
631
+ });
632
+ },
633
+ schedule: function(taskId, options) {
634
+ options = options || {};
635
+ return new Promise(function(resolve, reject) {
636
+ window._craftBgTaskResolve = resolve;
637
+ window._craftBgTaskReject = reject;
638
+ CraftAndroid.scheduleBackgroundTask(
639
+ taskId,
640
+ options.delay || 900,
641
+ options.requiresNetwork || false,
642
+ options.requiresCharging || false
643
+ );
644
+ });
645
+ },
646
+ cancel: function(taskId) {
647
+ return new Promise(function(resolve, reject) {
648
+ window._craftBgTaskResolve = resolve;
649
+ window._craftBgTaskReject = reject;
650
+ CraftAndroid.cancelBackgroundTask(taskId);
651
+ });
652
+ },
653
+ cancelAll: function() {
654
+ return new Promise(function(resolve, reject) {
655
+ window._craftBgTaskResolve = resolve;
656
+ window._craftBgTaskReject = reject;
657
+ CraftAndroid.cancelAllBackgroundTasks();
658
+ });
659
+ }
660
+ },
661
+
662
+ // PDF Viewer
663
+ openPDF: function(source, page) {
664
+ return new Promise(function(resolve, reject) {
665
+ window._craftPDFResolve = resolve;
666
+ window._craftPDFReject = reject;
667
+ CraftAndroid.openPDF(source, page || 0);
668
+ });
669
+ },
670
+ closePDF: function() {
671
+ return new Promise(function(resolve, reject) {
672
+ window._craftPDFResolve = resolve;
673
+ window._craftPDFReject = reject;
674
+ CraftAndroid.closePDF();
675
+ });
676
+ },
677
+
678
+ // Contacts Picker
679
+ pickContact: function(options) {
680
+ options = options || {};
681
+ return new Promise(function(resolve, reject) {
682
+ window._craftPickContactResolve = resolve;
683
+ window._craftPickContactReject = reject;
684
+ CraftAndroid.pickContact(options.multiple || false);
685
+ });
686
+ },
687
+
688
+ // App Shortcuts
689
+ shortcuts: {
690
+ set: function(shortcuts) {
691
+ return new Promise(function(resolve, reject) {
692
+ window._craftShortcutsResolve = resolve;
693
+ window._craftShortcutsReject = reject;
694
+ CraftAndroid.setShortcuts(JSON.stringify(shortcuts));
695
+ });
696
+ },
697
+ clear: function() {
698
+ return new Promise(function(resolve, reject) {
699
+ window._craftShortcutsResolve = resolve;
700
+ window._craftShortcutsReject = reject;
701
+ CraftAndroid.clearShortcuts();
702
+ });
703
+ },
704
+ onShortcut: function(callback) {
705
+ window.addEventListener('craftShortcut', function(e) { callback(e.detail); });
706
+ }
707
+ },
708
+
709
+ // Shared Preferences (Android equivalent of Keychain Sharing)
710
+ sharedKeychain: {
711
+ set: function(key, value, group) {
712
+ return new Promise(function(resolve, reject) {
713
+ window._craftSharedKeychainResolve = resolve;
714
+ window._craftSharedKeychainReject = reject;
715
+ CraftAndroid.setSharedItem(key, value, group || '');
716
+ });
717
+ },
718
+ get: function(key, group) {
719
+ return new Promise(function(resolve, reject) {
720
+ window._craftSharedKeychainResolve = resolve;
721
+ window._craftSharedKeychainReject = reject;
722
+ CraftAndroid.getSharedItem(key, group || '');
723
+ });
724
+ },
725
+ remove: function(key, group) {
726
+ return new Promise(function(resolve, reject) {
727
+ window._craftSharedKeychainResolve = resolve;
728
+ window._craftSharedKeychainReject = reject;
729
+ CraftAndroid.removeSharedItem(key, group || '');
730
+ });
731
+ }
732
+ },
733
+
734
+ // Local Auth Persistence
735
+ authPersistence: {
736
+ enable: function(duration) {
737
+ return new Promise(function(resolve, reject) {
738
+ window._craftAuthPersistResolve = resolve;
739
+ window._craftAuthPersistReject = reject;
740
+ CraftAndroid.setAuthPersistence(true, duration || 300);
741
+ });
742
+ },
743
+ disable: function() {
744
+ return new Promise(function(resolve, reject) {
745
+ window._craftAuthPersistResolve = resolve;
746
+ window._craftAuthPersistReject = reject;
747
+ CraftAndroid.setAuthPersistence(false, 0);
748
+ });
749
+ },
750
+ check: function() {
751
+ return new Promise(function(resolve, reject) {
752
+ window._craftAuthPersistResolve = resolve;
753
+ window._craftAuthPersistReject = reject;
754
+ CraftAndroid.checkAuthPersistence();
755
+ });
756
+ },
757
+ clear: function() {
758
+ return new Promise(function(resolve, reject) {
759
+ window._craftAuthPersistResolve = resolve;
760
+ window._craftAuthPersistReject = reject;
761
+ CraftAndroid.clearAuthPersistence();
762
+ });
763
+ }
764
+ },
765
+
766
+ // AR (ARCore) - Note: Full ARCore requires native Activity integration
767
+ ar: {
768
+ start: function(options) {
769
+ return new Promise(function(resolve, reject) {
770
+ window._craftARResolve = resolve;
771
+ window._craftARReject = reject;
772
+ CraftAndroid.startAR(JSON.stringify(options || {}));
773
+ });
774
+ },
775
+ stop: function() {
776
+ return new Promise(function(resolve, reject) {
777
+ window._craftARResolve = resolve;
778
+ window._craftARReject = reject;
779
+ CraftAndroid.stopAR();
780
+ });
781
+ },
782
+ placeObject: function(model, position) {
783
+ return new Promise(function(resolve, reject) {
784
+ window._craftARResolve = resolve;
785
+ window._craftARReject = reject;
786
+ CraftAndroid.placeARObject(model, JSON.stringify(position || {}));
787
+ });
788
+ },
789
+ removeObject: function(objectId) {
790
+ return new Promise(function(resolve, reject) {
791
+ window._craftARResolve = resolve;
792
+ window._craftARReject = reject;
793
+ CraftAndroid.removeARObject(objectId);
794
+ });
795
+ },
796
+ getPlanes: function() {
797
+ return new Promise(function(resolve, reject) {
798
+ window._craftARResolve = resolve;
799
+ window._craftARReject = reject;
800
+ CraftAndroid.getARPlanes();
801
+ });
802
+ },
803
+ onPlaneDetected: function(callback) {
804
+ window.addEventListener('craftARPlane', function(e) { callback(e.detail); });
805
+ }
806
+ },
807
+
808
+ // ML (ML Kit)
809
+ ml: {
810
+ classifyImage: function(imageBase64) {
811
+ return new Promise(function(resolve, reject) {
812
+ window._craftMLResolve = resolve;
813
+ window._craftMLReject = reject;
814
+ CraftAndroid.classifyImage(imageBase64);
815
+ });
816
+ },
817
+ detectObjects: function(imageBase64) {
818
+ return new Promise(function(resolve, reject) {
819
+ window._craftMLResolve = resolve;
820
+ window._craftMLReject = reject;
821
+ CraftAndroid.detectObjects(imageBase64);
822
+ });
823
+ },
824
+ recognizeText: function(imageBase64) {
825
+ return new Promise(function(resolve, reject) {
826
+ window._craftMLResolve = resolve;
827
+ window._craftMLReject = reject;
828
+ CraftAndroid.recognizeText(imageBase64);
829
+ });
830
+ }
831
+ },
832
+
833
+ // Deep Links
834
+ deepLinks: {
835
+ getInitialURL: function() {
836
+ return new Promise(function(resolve, reject) {
837
+ window._craftDeepLinkResolve = resolve;
838
+ window._craftDeepLinkReject = reject;
839
+ CraftAndroid.getInitialURL();
840
+ });
841
+ },
842
+ onLink: function(callback) {
843
+ window.addEventListener('craftDeepLink', function(e) { callback(e.detail); });
844
+ }
845
+ },
846
+
847
+ // Logging
848
+ log: function(msg) {
849
+ CraftAndroid.log(msg);
850
+ },
851
+
852
+ // Performance Profiling
853
+ _profiling: false,
854
+ _profilingData: null,
855
+ _profilingCallTimings: [],
856
+
857
+ startProfiling: function() {
858
+ var self = this;
859
+ this._profiling = true;
860
+ this._profilingData = {
861
+ startTime: Date.now(),
862
+ startMemory: null,
863
+ callTimings: [],
864
+ bridgeCalls: 0
865
+ };
866
+ this._profilingCallTimings = [];
867
+ // Get memory info
868
+ var memInfo = CraftAndroid.getMemoryUsage();
869
+ try { this._profilingData.startMemory = JSON.parse(memInfo); } catch(e) {}
870
+ console.log('[Craft] Profiling started');
871
+ return { started: true, timestamp: this._profilingData.startTime };
872
+ },
873
+
874
+ stopProfiling: function() {
875
+ if (!this._profiling || !this._profilingData) {
876
+ return null;
877
+ }
878
+ this._profiling = false;
879
+ var memInfo = null;
880
+ try { memInfo = JSON.parse(CraftAndroid.getMemoryUsage()); } catch(e) {}
881
+ var report = {
882
+ duration: Date.now() - this._profilingData.startTime,
883
+ bridgeCalls: this._profilingCallTimings.length,
884
+ callTimings: this._profilingCallTimings,
885
+ memory: {
886
+ start: this._profilingData.startMemory,
887
+ end: memInfo
888
+ },
889
+ avgCallTime: this._profilingCallTimings.length > 0
890
+ ? this._profilingCallTimings.reduce(function(a, b) { return a + b.duration; }, 0) / this._profilingCallTimings.length
891
+ : 0
892
+ };
893
+ this._profilingData = null;
894
+ console.log('[Craft] Profiling stopped', report);
895
+ return report;
896
+ },
897
+
898
+ _recordCallTiming: function(action, startTime, endTime) {
899
+ if (this._profiling) {
900
+ this._profilingCallTimings.push({
901
+ action: action,
902
+ startTime: startTime,
903
+ endTime: endTime,
904
+ duration: endTime - startTime
905
+ });
906
+ }
907
+ },
908
+
909
+ getProfilingData: function() {
910
+ if (!this._profilingData) return null;
911
+ return {
912
+ running: this._profiling,
913
+ duration: Date.now() - this._profilingData.startTime,
914
+ bridgeCalls: this._profilingCallTimings.length,
915
+ callTimings: this._profilingCallTimings
916
+ };
917
+ },
918
+
919
+ // OTA Updates
920
+ ota: {
921
+ _config: null,
922
+ _status: 'idle',
923
+ _progressCallbacks: [],
924
+ _statusCallbacks: [],
925
+
926
+ configure: function(options) {
927
+ this._config = options;
928
+ CraftAndroid.otaConfigure(JSON.stringify(options));
929
+ },
930
+ checkForUpdate: function() {
931
+ return new Promise(function(resolve, reject) {
932
+ window._craftOTACheckResolve = resolve;
933
+ window._craftOTACheckReject = reject;
934
+ CraftAndroid.otaCheckForUpdate();
935
+ });
936
+ },
937
+ downloadUpdate: function(options) {
938
+ return new Promise(function(resolve, reject) {
939
+ window._craftOTADownloadResolve = resolve;
940
+ window._craftOTADownloadReject = reject;
941
+ CraftAndroid.otaDownloadUpdate(JSON.stringify(options || {}));
942
+ });
943
+ },
944
+ applyUpdate: function() {
945
+ return new Promise(function(resolve, reject) {
946
+ window._craftOTAApplyResolve = resolve;
947
+ window._craftOTAApplyReject = reject;
948
+ CraftAndroid.otaApplyUpdate();
949
+ });
950
+ },
951
+ rollback: function() {
952
+ return new Promise(function(resolve, reject) {
953
+ window._craftOTARollbackResolve = resolve;
954
+ window._craftOTARollbackReject = reject;
955
+ CraftAndroid.otaRollback();
956
+ });
957
+ },
958
+ getCurrentBundle: function() {
959
+ var bundleJson = CraftAndroid.otaGetCurrentBundle();
960
+ try { return JSON.parse(bundleJson); }
961
+ catch(e) { return { version: '1.0.0', buildNumber: 1, hash: '', isOriginal: true, installedAt: '' }; }
962
+ },
963
+ onProgress: function(callback) {
964
+ this._progressCallbacks.push(callback);
965
+ window.addEventListener('craftOTAProgress', function(e) { callback(e.detail); });
966
+ },
967
+ onStatusChange: function(callback) {
968
+ this._statusCallbacks.push(callback);
969
+ window.addEventListener('craftOTAStatus', function(e) { callback(e.detail.status); });
970
+ }
971
+ },
972
+
973
+ // Debug Mode
974
+ _debug: false,
975
+ _lastError: null,
976
+ _errorHistory: [],
977
+ _callLog: [],
978
+ _networkLog: [],
979
+ _consoleLog: [],
980
+ _originalConsole: null,
981
+
982
+ // Error code mappings for user-friendly messages
983
+ _errorMessages: {
984
+ 'PERMISSION_DENIED': 'Permission was denied. Please grant access in Settings.',
985
+ 'NOT_AVAILABLE': 'This feature is not available on this device.',
986
+ 'CANCELLED': 'The operation was cancelled by the user.',
987
+ 'NETWORK_ERROR': 'A network error occurred. Please check your connection.',
988
+ 'TIMEOUT': 'The operation timed out. Please try again.',
989
+ 'INVALID_PARAMS': 'Invalid parameters provided.',
990
+ 'NOT_FOUND': 'The requested resource was not found.',
991
+ 'AUTH_FAILED': 'Authentication failed.',
992
+ 'STORAGE_FULL': 'Storage is full. Please free up space.',
993
+ 'CRAFT_ERROR': 'An unexpected error occurred.'
994
+ },
995
+
996
+ // Get user-friendly error message
997
+ getErrorMessage: function(code) {
998
+ return this._errorMessages[code] || this._errorMessages['CRAFT_ERROR'];
999
+ },
1000
+
1001
+ // Store error for debugging
1002
+ _storeError: function(error, code, nativeStack) {
1003
+ this._lastError = {
1004
+ message: error,
1005
+ code: code || 'CRAFT_ERROR',
1006
+ timestamp: Date.now(),
1007
+ nativeStack: nativeStack || null,
1008
+ jsStack: new Error().stack
1009
+ };
1010
+ if (this._debug) {
1011
+ window.dispatchEvent(new CustomEvent('craftError', {detail: this._lastError}));
1012
+ console.error('[Craft Error]', code, error);
1013
+ }
1014
+ this._errorHistory.push(this._lastError);
1015
+ if (this._errorHistory.length > 50) this._errorHistory.shift();
1016
+ },
1017
+
1018
+ // Get error history
1019
+ getErrorHistory: function() {
1020
+ return this._errorHistory.slice();
1021
+ },
1022
+
1023
+ // Clear error history
1024
+ clearErrorHistory: function() {
1025
+ this._errorHistory = [];
1026
+ this._lastError = null;
1027
+ },
1028
+
1029
+ // Get last error details
1030
+ getLastError: function() {
1031
+ return this._lastError;
1032
+ },
1033
+
1034
+ // Enable/disable debug mode
1035
+ setDebugMode: function(enabled) {
1036
+ this._debug = enabled;
1037
+ if (enabled) {
1038
+ this._callLog = [];
1039
+ this._networkLog = [];
1040
+ this._setupConsoleCapture();
1041
+ this._setupNetworkInspector();
1042
+ console.log('[Craft] Debug mode enabled');
1043
+ } else {
1044
+ this._restoreConsole();
1045
+ }
1046
+ },
1047
+
1048
+ // Setup console capture
1049
+ _setupConsoleCapture: function() {
1050
+ if (this._originalConsole) return;
1051
+ var self = this;
1052
+ this._originalConsole = {
1053
+ log: console.log,
1054
+ warn: console.warn,
1055
+ error: console.error,
1056
+ info: console.info,
1057
+ debug: console.debug
1058
+ };
1059
+ ['log', 'warn', 'error', 'info', 'debug'].forEach(function(level) {
1060
+ console[level] = function() {
1061
+ var args = Array.prototype.slice.call(arguments);
1062
+ self._consoleLog.push({
1063
+ level: level,
1064
+ args: args.map(function(a) {
1065
+ try { return typeof a === 'object' ? JSON.stringify(a) : String(a); }
1066
+ catch(e) { return String(a); }
1067
+ }),
1068
+ timestamp: Date.now()
1069
+ });
1070
+ if (self._consoleLog.length > 200) self._consoleLog.shift();
1071
+ self._originalConsole[level].apply(console, arguments);
1072
+ };
1073
+ });
1074
+ },
1075
+
1076
+ // Restore original console
1077
+ _restoreConsole: function() {
1078
+ if (!this._originalConsole) return;
1079
+ console.log = this._originalConsole.log;
1080
+ console.warn = this._originalConsole.warn;
1081
+ console.error = this._originalConsole.error;
1082
+ console.info = this._originalConsole.info;
1083
+ console.debug = this._originalConsole.debug;
1084
+ this._originalConsole = null;
1085
+ },
1086
+
1087
+ // Setup network inspector
1088
+ _setupNetworkInspector: function() {
1089
+ var self = this;
1090
+ if (window._craftNetworkSetup) return;
1091
+ window._craftNetworkSetup = true;
1092
+
1093
+ var originalFetch = window.fetch;
1094
+ window.fetch = function(url, options) {
1095
+ var startTime = Date.now();
1096
+ var entry = {
1097
+ type: 'fetch',
1098
+ url: typeof url === 'string' ? url : url.url,
1099
+ method: (options && options.method) || 'GET',
1100
+ startTime: startTime,
1101
+ status: null,
1102
+ duration: null
1103
+ };
1104
+ self._networkLog.push(entry);
1105
+ if (self._networkLog.length > 100) self._networkLog.shift();
1106
+
1107
+ return originalFetch.apply(window, arguments).then(function(response) {
1108
+ entry.status = response.status;
1109
+ entry.duration = Date.now() - startTime;
1110
+ return response;
1111
+ }).catch(function(err) {
1112
+ entry.status = 'error';
1113
+ entry.error = err.message;
1114
+ entry.duration = Date.now() - startTime;
1115
+ throw err;
1116
+ });
1117
+ };
1118
+
1119
+ var OriginalXHR = window.XMLHttpRequest;
1120
+ window.XMLHttpRequest = function() {
1121
+ var xhr = new OriginalXHR();
1122
+ var entry = { type: 'xhr', url: '', method: '', startTime: null, status: null, duration: null };
1123
+
1124
+ var originalOpen = xhr.open;
1125
+ xhr.open = function(method, url) {
1126
+ entry.method = method;
1127
+ entry.url = url;
1128
+ return originalOpen.apply(xhr, arguments);
1129
+ };
1130
+
1131
+ var originalSend = xhr.send;
1132
+ xhr.send = function() {
1133
+ entry.startTime = Date.now();
1134
+ self._networkLog.push(entry);
1135
+ if (self._networkLog.length > 100) self._networkLog.shift();
1136
+
1137
+ xhr.addEventListener('loadend', function() {
1138
+ entry.status = xhr.status;
1139
+ entry.duration = Date.now() - entry.startTime;
1140
+ });
1141
+ return originalSend.apply(xhr, arguments);
1142
+ };
1143
+ return xhr;
1144
+ };
1145
+ },
1146
+
1147
+ // Get console log
1148
+ getConsoleLog: function() {
1149
+ return this._consoleLog.slice();
1150
+ },
1151
+
1152
+ // Clear console log
1153
+ clearConsoleLog: function() {
1154
+ this._consoleLog = [];
1155
+ },
1156
+
1157
+ // Get network log
1158
+ getNetworkLog: function() {
1159
+ return this._networkLog.slice();
1160
+ },
1161
+
1162
+ // Clear network log
1163
+ clearNetworkLog: function() {
1164
+ this._networkLog = [];
1165
+ },
1166
+
1167
+ // Get call log
1168
+ getCallLog: function() {
1169
+ return this._callLog;
1170
+ },
1171
+
1172
+ // Clear call log
1173
+ clearCallLog: function() {
1174
+ this._callLog = [];
1175
+ },
1176
+
1177
+ // Internal: log bridge call
1178
+ _logCall: function(action, params) {
1179
+ if (this._debug) {
1180
+ var entry = {
1181
+ action: action,
1182
+ params: params,
1183
+ timestamp: Date.now()
1184
+ };
1185
+ this._callLog.push(entry);
1186
+ if (this._callLog.length > 100) this._callLog.shift();
1187
+ console.log('[Craft] ' + action, params);
1188
+ }
1189
+ },
1190
+
1191
+ // Get full debug report
1192
+ getDebugReport: function() {
1193
+ return {
1194
+ enabled: this._debug,
1195
+ lastError: this._lastError,
1196
+ errorHistory: this._errorHistory.slice(-10),
1197
+ callLog: this._callLog.slice(-20),
1198
+ networkLog: this._networkLog.slice(-20),
1199
+ consoleLog: this._consoleLog.slice(-50),
1200
+ timestamp: Date.now()
1201
+ };
1202
+ }
1203
+ };
1204
+
1205
+ // Dispatch ready event
1206
+ window.dispatchEvent(new CustomEvent('craftReady', {detail: window.craft}));
1207
+ console.log('Craft Android bridge initialized');
1208
+ """.trimIndent()
1209
+
1210
+ activity.runOnUiThread {
1211
+ webView.evaluateJavascript(script, null)
1212
+ }
1213
+ }
1214
+
1215
+ // ==================== Haptics ====================
1216
+
1217
+ @JavascriptInterface
1218
+ fun haptic(style: String) {
1219
+ val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1220
+ val vibratorManager = activity.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
1221
+ vibratorManager.defaultVibrator
1222
+ } else {
1223
+ @Suppress("DEPRECATION")
1224
+ activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
1225
+ }
1226
+
1227
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1228
+ val effect = when (style) {
1229
+ "light" -> VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE)
1230
+ "medium" -> VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)
1231
+ "heavy" -> VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)
1232
+ "success" -> VibrationEffect.createWaveform(longArrayOf(0, 30, 50, 30), -1)
1233
+ "warning" -> VibrationEffect.createWaveform(longArrayOf(0, 50, 100, 50), -1)
1234
+ "error" -> VibrationEffect.createWaveform(longArrayOf(0, 100, 50, 100), -1)
1235
+ "selection" -> VibrationEffect.createOneShot(5, VibrationEffect.DEFAULT_AMPLITUDE)
1236
+ else -> VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)
1237
+ }
1238
+ vibrator.vibrate(effect)
1239
+ } else {
1240
+ @Suppress("DEPRECATION")
1241
+ vibrator.vibrate(20)
1242
+ }
1243
+ }
1244
+
1245
+ // ==================== Speech Recognition ====================
1246
+
1247
+ @JavascriptInterface
1248
+ fun startListening() {
1249
+ if (!SpeechRecognizer.isRecognitionAvailable(activity)) {
1250
+ sendEvent("craftSpeechError", mapOf("error" to "Speech recognition not available"))
1251
+ return
1252
+ }
1253
+
1254
+ activity.runOnUiThread {
1255
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
1256
+ != PackageManager.PERMISSION_GRANTED) {
1257
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO), 100)
1258
+ return@runOnUiThread
1259
+ }
1260
+
1261
+ speechRecognizer = SpeechRecognizer.createSpeechRecognizer(activity)
1262
+ speechRecognizer?.setRecognitionListener(object : RecognitionListener {
1263
+ override fun onReadyForSpeech(params: Bundle?) {
1264
+ isListening = true
1265
+ sendEvent("craftSpeechStart", emptyMap())
1266
+ haptic("light")
1267
+ }
1268
+
1269
+ override fun onBeginningOfSpeech() {}
1270
+ override fun onRmsChanged(rmsdB: Float) {}
1271
+ override fun onBufferReceived(buffer: ByteArray?) {}
1272
+ override fun onEndOfSpeech() {}
1273
+
1274
+ override fun onError(error: Int) {
1275
+ isListening = false
1276
+ val errorMsg = when (error) {
1277
+ SpeechRecognizer.ERROR_AUDIO -> "Audio recording error"
1278
+ SpeechRecognizer.ERROR_CLIENT -> "Client error"
1279
+ SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "Insufficient permissions"
1280
+ SpeechRecognizer.ERROR_NETWORK -> "Network error"
1281
+ SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> "Network timeout"
1282
+ SpeechRecognizer.ERROR_NO_MATCH -> "No match"
1283
+ SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> "Recognizer busy"
1284
+ SpeechRecognizer.ERROR_SERVER -> "Server error"
1285
+ SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "Speech timeout"
1286
+ else -> "Unknown error"
1287
+ }
1288
+ sendEvent("craftSpeechError", mapOf("error" to errorMsg))
1289
+ haptic("light")
1290
+ }
1291
+
1292
+ override fun onResults(results: Bundle?) {
1293
+ isListening = false
1294
+ val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
1295
+ val transcript = matches?.firstOrNull() ?: ""
1296
+ sendEvent("craftSpeechResult", mapOf("transcript" to transcript, "isFinal" to true))
1297
+ sendEvent("craftSpeechEnd", emptyMap())
1298
+ haptic("light")
1299
+ }
1300
+
1301
+ override fun onPartialResults(partialResults: Bundle?) {
1302
+ val matches = partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
1303
+ val transcript = matches?.firstOrNull() ?: ""
1304
+ sendEvent("craftSpeechResult", mapOf("transcript" to transcript, "isFinal" to false))
1305
+ }
1306
+
1307
+ override fun onEvent(eventType: Int, params: Bundle?) {}
1308
+ })
1309
+
1310
+ val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
1311
+ putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
1312
+ putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
1313
+ putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
1314
+ putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
1315
+ }
1316
+
1317
+ speechRecognizer?.startListening(intent)
1318
+ }
1319
+ }
1320
+
1321
+ @JavascriptInterface
1322
+ fun stopListening() {
1323
+ activity.runOnUiThread {
1324
+ speechRecognizer?.stopListening()
1325
+ speechRecognizer?.destroy()
1326
+ speechRecognizer = null
1327
+ isListening = false
1328
+ }
1329
+ }
1330
+
1331
+ // ==================== Share ====================
1332
+
1333
+ @JavascriptInterface
1334
+ fun share(text: String, title: String) {
1335
+ val intent = Intent(Intent.ACTION_SEND).apply {
1336
+ type = "text/plain"
1337
+ putExtra(Intent.EXTRA_TEXT, text)
1338
+ if (title.isNotEmpty()) {
1339
+ putExtra(Intent.EXTRA_SUBJECT, title)
1340
+ }
1341
+ }
1342
+ activity.startActivity(Intent.createChooser(intent, "Share"))
1343
+ }
1344
+
1345
+ // ==================== Camera & Gallery ====================
1346
+
1347
+ @JavascriptInterface
1348
+ fun openCamera() {
1349
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
1350
+ != PackageManager.PERMISSION_GRANTED) {
1351
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), 101)
1352
+ return
1353
+ }
1354
+
1355
+ val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
1356
+ activity.startActivityForResult(intent, REQUEST_CAMERA)
1357
+ }
1358
+
1359
+ @JavascriptInterface
1360
+ fun pickImage() {
1361
+ val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
1362
+ activity.startActivityForResult(intent, REQUEST_GALLERY)
1363
+ }
1364
+
1365
+ fun handleImageResult(requestCode: Int, resultCode: Int, data: Intent?) {
1366
+ if (resultCode != Activity.RESULT_OK) {
1367
+ val event = if (requestCode == REQUEST_CAMERA) "_craftCameraReject" else "_craftGalleryReject"
1368
+ activity.runOnUiThread {
1369
+ webView.evaluateJavascript("window.$event && window.$event('Cancelled')", null)
1370
+ }
1371
+ return
1372
+ }
1373
+
1374
+ val imageUri: String? = when (requestCode) {
1375
+ REQUEST_CAMERA -> {
1376
+ // For camera, we get a bitmap thumbnail
1377
+ val bitmap = data?.extras?.get("data") as? Bitmap
1378
+ bitmap?.let { "data:image/png;base64,${bitmapToBase64(it)}" }
1379
+ }
1380
+ REQUEST_GALLERY -> {
1381
+ data?.data?.toString()
1382
+ }
1383
+ else -> null
1384
+ }
1385
+
1386
+ val event = if (requestCode == REQUEST_CAMERA) "_craftCameraResolve" else "_craftGalleryResolve"
1387
+ activity.runOnUiThread {
1388
+ webView.evaluateJavascript("window.$event && window.$event('$imageUri')", null)
1389
+ }
1390
+ }
1391
+
1392
+ private fun bitmapToBase64(bitmap: Bitmap): String {
1393
+ val outputStream = java.io.ByteArrayOutputStream()
1394
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
1395
+ return android.util.Base64.encodeToString(outputStream.toByteArray(), android.util.Base64.NO_WRAP)
1396
+ }
1397
+
1398
+ // ==================== Biometric Auth ====================
1399
+
1400
+ private fun isBiometricAvailable(): Boolean {
1401
+ val biometricManager = BiometricManager.from(activity)
1402
+ return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
1403
+ BiometricManager.BIOMETRIC_SUCCESS
1404
+ }
1405
+
1406
+ @JavascriptInterface
1407
+ fun authenticate(reason: String) {
1408
+ if (activity !is FragmentActivity) {
1409
+ activity.runOnUiThread {
1410
+ webView.evaluateJavascript(
1411
+ "window._craftBiometricReject && window._craftBiometricReject('Activity not supported')",
1412
+ null
1413
+ )
1414
+ }
1415
+ return
1416
+ }
1417
+
1418
+ activity.runOnUiThread {
1419
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
1420
+ .setTitle("Authenticate")
1421
+ .setSubtitle(reason)
1422
+ .setNegativeButtonText("Cancel")
1423
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
1424
+ .build()
1425
+
1426
+ val biometricPrompt = BiometricPrompt(
1427
+ activity as FragmentActivity,
1428
+ mainExecutor,
1429
+ object : BiometricPrompt.AuthenticationCallback() {
1430
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
1431
+ webView.evaluateJavascript(
1432
+ "window._craftBiometricResolve && window._craftBiometricResolve(true)",
1433
+ null
1434
+ )
1435
+ }
1436
+
1437
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
1438
+ webView.evaluateJavascript(
1439
+ "window._craftBiometricReject && window._craftBiometricReject('$errString')",
1440
+ null
1441
+ )
1442
+ }
1443
+
1444
+ override fun onAuthenticationFailed() {
1445
+ // Don't reject yet - user can retry
1446
+ }
1447
+ }
1448
+ )
1449
+
1450
+ biometricPrompt.authenticate(promptInfo)
1451
+ }
1452
+ }
1453
+
1454
+ // ==================== Push Notifications ====================
1455
+
1456
+ @JavascriptInterface
1457
+ fun registerPush() {
1458
+ // This would integrate with Firebase Cloud Messaging
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
+ }
1476
+ }
1477
+
1478
+ // ==================== Secure Storage ====================
1479
+
1480
+ @JavascriptInterface
1481
+ fun secureSet(key: String, value: String): Boolean {
1482
+ return try {
1483
+ securePrefs.edit().putString(key, value).apply()
1484
+ true
1485
+ } catch (e: Exception) {
1486
+ false
1487
+ }
1488
+ }
1489
+
1490
+ @JavascriptInterface
1491
+ fun secureGet(key: String): String? {
1492
+ return try {
1493
+ securePrefs.getString(key, null)
1494
+ } catch (e: Exception) {
1495
+ null
1496
+ }
1497
+ }
1498
+
1499
+ @JavascriptInterface
1500
+ fun secureRemove(key: String): Boolean {
1501
+ return try {
1502
+ securePrefs.edit().remove(key).apply()
1503
+ true
1504
+ } catch (e: Exception) {
1505
+ false
1506
+ }
1507
+ }
1508
+
1509
+ // ==================== Logging ====================
1510
+
1511
+ @JavascriptInterface
1512
+ fun log(message: String) {
1513
+ android.util.Log.d("CraftBridge", message)
1514
+ }
1515
+
1516
+ // ==================== Geolocation ====================
1517
+
1518
+ @JavascriptInterface
1519
+ fun getCurrentPosition(optionsJson: String) {
1520
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)
1521
+ != PackageManager.PERMISSION_GRANTED) {
1522
+ ActivityCompat.requestPermissions(
1523
+ activity,
1524
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
1525
+ REQUEST_LOCATION
1526
+ )
1527
+ activity.runOnUiThread {
1528
+ webView.evaluateJavascript(
1529
+ "window._craftLocationReject && window._craftLocationReject({code: 1, message: 'Permission denied'})",
1530
+ null
1531
+ )
1532
+ }
1533
+ return
1534
+ }
1535
+
1536
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
1537
+ fusedLocationClient?.lastLocation?.addOnSuccessListener { location: Location? ->
1538
+ if (location != null) {
1539
+ val json = JSONObject().apply {
1540
+ put("latitude", location.latitude)
1541
+ put("longitude", location.longitude)
1542
+ put("accuracy", location.accuracy)
1543
+ put("altitude", location.altitude)
1544
+ put("speed", location.speed)
1545
+ put("heading", location.bearing)
1546
+ put("timestamp", location.time)
1547
+ }
1548
+ activity.runOnUiThread {
1549
+ webView.evaluateJavascript(
1550
+ "window._craftLocationResolve && window._craftLocationResolve($json)",
1551
+ null
1552
+ )
1553
+ }
1554
+ } else {
1555
+ // Request fresh location if last location is null
1556
+ requestFreshLocation()
1557
+ }
1558
+ }?.addOnFailureListener { e ->
1559
+ activity.runOnUiThread {
1560
+ webView.evaluateJavascript(
1561
+ "window._craftLocationReject && window._craftLocationReject({code: 2, message: '${e.message}'})",
1562
+ null
1563
+ )
1564
+ }
1565
+ }
1566
+ }
1567
+
1568
+ private fun requestFreshLocation() {
1569
+ if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
1570
+ return
1571
+ }
1572
+
1573
+ val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000)
1574
+ .setWaitForAccurateLocation(false)
1575
+ .setMinUpdateIntervalMillis(500)
1576
+ .setMaxUpdateDelayMillis(1000)
1577
+ .setMaxUpdates(1)
1578
+ .build()
1579
+
1580
+ val callback = object : LocationCallback() {
1581
+ override fun onLocationResult(result: LocationResult) {
1582
+ fusedLocationClient?.removeLocationUpdates(this)
1583
+ val location = result.lastLocation
1584
+ if (location != null) {
1585
+ val json = JSONObject().apply {
1586
+ put("latitude", location.latitude)
1587
+ put("longitude", location.longitude)
1588
+ put("accuracy", location.accuracy)
1589
+ put("altitude", location.altitude)
1590
+ put("speed", location.speed)
1591
+ put("heading", location.bearing)
1592
+ put("timestamp", location.time)
1593
+ }
1594
+ activity.runOnUiThread {
1595
+ webView.evaluateJavascript(
1596
+ "window._craftLocationResolve && window._craftLocationResolve($json)",
1597
+ null
1598
+ )
1599
+ }
1600
+ }
1601
+ }
1602
+ }
1603
+
1604
+ fusedLocationClient?.requestLocationUpdates(locationRequest, callback, Looper.getMainLooper())
1605
+ }
1606
+
1607
+ @JavascriptInterface
1608
+ fun watchPosition(optionsJson: String): Int {
1609
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)
1610
+ != PackageManager.PERMISSION_GRANTED) {
1611
+ return -1
1612
+ }
1613
+
1614
+ val currentWatchId = ++watchId
1615
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
1616
+
1617
+ val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000)
1618
+ .setMinUpdateIntervalMillis(2000)
1619
+ .build()
1620
+
1621
+ locationCallback = object : LocationCallback() {
1622
+ override fun onLocationResult(result: LocationResult) {
1623
+ val location = result.lastLocation ?: return
1624
+ val json = JSONObject().apply {
1625
+ put("latitude", location.latitude)
1626
+ put("longitude", location.longitude)
1627
+ put("accuracy", location.accuracy)
1628
+ put("altitude", location.altitude)
1629
+ put("speed", location.speed)
1630
+ put("heading", location.bearing)
1631
+ put("timestamp", location.time)
1632
+ }
1633
+ activity.runOnUiThread {
1634
+ webView.evaluateJavascript(
1635
+ "window._craftLocationWatch_$currentWatchId && window._craftLocationWatch_$currentWatchId($json)",
1636
+ null
1637
+ )
1638
+ }
1639
+ }
1640
+ }
1641
+
1642
+ if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
1643
+ fusedLocationClient?.requestLocationUpdates(locationRequest, locationCallback!!, Looper.getMainLooper())
1644
+ }
1645
+
1646
+ return currentWatchId
1647
+ }
1648
+
1649
+ @JavascriptInterface
1650
+ fun clearWatch(watchId: Int) {
1651
+ locationCallback?.let {
1652
+ fusedLocationClient?.removeLocationUpdates(it)
1653
+ }
1654
+ locationCallback = null
1655
+ }
1656
+
1657
+ // ==================== Clipboard ====================
1658
+
1659
+ @JavascriptInterface
1660
+ fun clipboardRead(): String {
1661
+ val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
1662
+ val clip = clipboard.primaryClip
1663
+ return if (clip != null && clip.itemCount > 0) {
1664
+ clip.getItemAt(0).text?.toString() ?: ""
1665
+ } else {
1666
+ ""
1667
+ }
1668
+ }
1669
+
1670
+ @JavascriptInterface
1671
+ fun clipboardWrite(text: String): Boolean {
1672
+ return try {
1673
+ val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
1674
+ val clip = ClipData.newPlainText("Craft", text)
1675
+ clipboard.setPrimaryClip(clip)
1676
+ true
1677
+ } catch (e: Exception) {
1678
+ false
1679
+ }
1680
+ }
1681
+
1682
+ // ==================== Device Info ====================
1683
+
1684
+ @JavascriptInterface
1685
+ fun getDeviceInfo(): String {
1686
+ val metrics = DisplayMetrics()
1687
+ @Suppress("DEPRECATION")
1688
+ activity.windowManager.defaultDisplay.getMetrics(metrics)
1689
+
1690
+ return JSONObject().apply {
1691
+ put("model", Build.MODEL)
1692
+ put("manufacturer", Build.MANUFACTURER)
1693
+ put("brand", Build.BRAND)
1694
+ put("device", Build.DEVICE)
1695
+ put("systemVersion", Build.VERSION.RELEASE)
1696
+ put("sdkVersion", Build.VERSION.SDK_INT)
1697
+ put("screenWidth", metrics.widthPixels)
1698
+ put("screenHeight", metrics.heightPixels)
1699
+ put("density", metrics.density)
1700
+ put("platform", "android")
1701
+ put("appVersion", try {
1702
+ activity.packageManager.getPackageInfo(activity.packageName, 0).versionName
1703
+ } catch (e: Exception) { "unknown" })
1704
+ put("appBuild", try {
1705
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1706
+ activity.packageManager.getPackageInfo(activity.packageName, 0).longVersionCode
1707
+ } else {
1708
+ @Suppress("DEPRECATION")
1709
+ activity.packageManager.getPackageInfo(activity.packageName, 0).versionCode
1710
+ }
1711
+ } catch (e: Exception) { 0 })
1712
+ put("isEmulator", Build.FINGERPRINT.contains("generic") || Build.FINGERPRINT.contains("emulator"))
1713
+ }.toString()
1714
+ }
1715
+
1716
+ // ==================== App Badge ====================
1717
+
1718
+ @JavascriptInterface
1719
+ fun setBadge(count: Int) {
1720
+ // Android doesn't have native badge support, but we can use notification channels
1721
+ // This is a simplified implementation - for full support, use ShortcutBadger library
1722
+ try {
1723
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1724
+ val notificationManager = NotificationManagerCompat.from(activity)
1725
+ // Badge is typically shown via notification count
1726
+ // Full implementation would require creating a notification
1727
+ }
1728
+ } catch (e: Exception) {
1729
+ log("Badge not supported: ${e.message}")
1730
+ }
1731
+ }
1732
+
1733
+ @JavascriptInterface
1734
+ fun clearBadge() {
1735
+ setBadge(0)
1736
+ }
1737
+
1738
+ // ==================== Network Status ====================
1739
+
1740
+ @JavascriptInterface
1741
+ fun getNetworkStatus(): String {
1742
+ val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
1743
+ val network = connectivityManager.activeNetwork
1744
+ val capabilities = connectivityManager.getNetworkCapabilities(network)
1745
+
1746
+ val isConnected = capabilities != null
1747
+ val connectionType = when {
1748
+ capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "wifi"
1749
+ capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "cellular"
1750
+ capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true -> "ethernet"
1751
+ else -> "none"
1752
+ }
1753
+
1754
+ return JSONObject().apply {
1755
+ put("isConnected", isConnected)
1756
+ put("type", connectionType)
1757
+ put("isWifi", connectionType == "wifi")
1758
+ put("isCellular", connectionType == "cellular")
1759
+ }.toString()
1760
+ }
1761
+
1762
+ @JavascriptInterface
1763
+ fun startNetworkMonitoring() {
1764
+ val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
1765
+
1766
+ networkCallback = object : ConnectivityManager.NetworkCallback() {
1767
+ override fun onAvailable(network: Network) {
1768
+ sendNetworkChange()
1769
+ }
1770
+
1771
+ override fun onLost(network: Network) {
1772
+ sendNetworkChange()
1773
+ }
1774
+
1775
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
1776
+ sendNetworkChange()
1777
+ }
1778
+ }
1779
+
1780
+ val request = NetworkRequest.Builder()
1781
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
1782
+ .build()
1783
+
1784
+ connectivityManager.registerNetworkCallback(request, networkCallback!!)
1785
+ }
1786
+
1787
+ @JavascriptInterface
1788
+ fun stopNetworkMonitoring() {
1789
+ val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
1790
+ networkCallback?.let {
1791
+ connectivityManager.unregisterNetworkCallback(it)
1792
+ }
1793
+ networkCallback = null
1794
+ }
1795
+
1796
+ private fun sendNetworkChange() {
1797
+ val status = getNetworkStatus()
1798
+ activity.runOnUiThread {
1799
+ webView.evaluateJavascript(
1800
+ "window._craftNetworkChangeCallback && window._craftNetworkChangeCallback($status)",
1801
+ null
1802
+ )
1803
+ }
1804
+ }
1805
+
1806
+ // ==================== App Review ====================
1807
+
1808
+ @JavascriptInterface
1809
+ fun requestReview() {
1810
+ val reviewManager = ReviewManagerFactory.create(activity)
1811
+ val request = reviewManager.requestReviewFlow()
1812
+
1813
+ request.addOnCompleteListener { task ->
1814
+ if (task.isSuccessful) {
1815
+ val reviewInfo = task.result
1816
+ val flow = reviewManager.launchReviewFlow(activity, reviewInfo)
1817
+ flow.addOnCompleteListener {
1818
+ activity.runOnUiThread {
1819
+ webView.evaluateJavascript(
1820
+ "window._craftReviewResolve && window._craftReviewResolve(true)",
1821
+ null
1822
+ )
1823
+ }
1824
+ }
1825
+ } else {
1826
+ activity.runOnUiThread {
1827
+ webView.evaluateJavascript(
1828
+ "window._craftReviewReject && window._craftReviewReject('${task.exception?.message ?: \"Review flow failed\"}')",
1829
+ null
1830
+ )
1831
+ }
1832
+ }
1833
+ }
1834
+ }
1835
+
1836
+ // ==================== Flashlight ====================
1837
+
1838
+ private fun hasFlashlight(): Boolean {
1839
+ return activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
1840
+ }
1841
+
1842
+ @JavascriptInterface
1843
+ fun setFlashlight(on: Boolean): Boolean {
1844
+ return try {
1845
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1846
+ val cameraManager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
1847
+ val cameraId = cameraManager.cameraIdList.firstOrNull { id ->
1848
+ cameraManager.getCameraCharacteristics(id)
1849
+ .get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
1850
+ }
1851
+ if (cameraId != null) {
1852
+ cameraManager.setTorchMode(cameraId, on)
1853
+ isFlashlightOn = on
1854
+ true
1855
+ } else {
1856
+ false
1857
+ }
1858
+ } else {
1859
+ false
1860
+ }
1861
+ } catch (e: CameraAccessException) {
1862
+ false
1863
+ }
1864
+ }
1865
+
1866
+ @JavascriptInterface
1867
+ fun toggleFlashlight(): Boolean {
1868
+ isFlashlightOn = !isFlashlightOn
1869
+ return setFlashlight(isFlashlightOn)
1870
+ }
1871
+
1872
+ // ==================== Open URL ====================
1873
+
1874
+ @JavascriptInterface
1875
+ fun openURL(url: String): Boolean {
1876
+ return try {
1877
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
1878
+ activity.startActivity(intent)
1879
+ true
1880
+ } catch (e: Exception) {
1881
+ false
1882
+ }
1883
+ }
1884
+
1885
+ // ==================== Vibration Pattern ====================
1886
+
1887
+ @JavascriptInterface
1888
+ fun vibrate(patternJson: String) {
1889
+ val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1890
+ val vibratorManager = activity.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
1891
+ vibratorManager.defaultVibrator
1892
+ } else {
1893
+ @Suppress("DEPRECATION")
1894
+ activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
1895
+ }
1896
+
1897
+ try {
1898
+ val pattern = org.json.JSONArray(patternJson)
1899
+ val timings = LongArray(pattern.length()) { i -> pattern.getLong(i) }
1900
+
1901
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1902
+ val effect = VibrationEffect.createWaveform(timings, -1)
1903
+ vibrator.vibrate(effect)
1904
+ } else {
1905
+ @Suppress("DEPRECATION")
1906
+ vibrator.vibrate(timings, -1)
1907
+ }
1908
+ } catch (e: Exception) {
1909
+ log("Vibration error: ${e.message}")
1910
+ }
1911
+ }
1912
+
1913
+ // ==================== App State ====================
1914
+
1915
+ @JavascriptInterface
1916
+ fun getAppState(): String {
1917
+ return currentAppState
1918
+ }
1919
+
1920
+ @JavascriptInterface
1921
+ fun startAppStateMonitoring() {
1922
+ activity.runOnUiThread {
1923
+ ProcessLifecycleOwner.get().lifecycle.addObserver(appStateObserver)
1924
+ }
1925
+ }
1926
+
1927
+ @JavascriptInterface
1928
+ fun stopAppStateMonitoring() {
1929
+ activity.runOnUiThread {
1930
+ ProcessLifecycleOwner.get().lifecycle.removeObserver(appStateObserver)
1931
+ }
1932
+ }
1933
+
1934
+ private val appStateObserver = LifecycleEventObserver { _, event ->
1935
+ val newState = when (event) {
1936
+ Lifecycle.Event.ON_START, Lifecycle.Event.ON_RESUME -> "active"
1937
+ Lifecycle.Event.ON_PAUSE -> "inactive"
1938
+ Lifecycle.Event.ON_STOP -> "background"
1939
+ else -> currentAppState
1940
+ }
1941
+
1942
+ if (newState != currentAppState) {
1943
+ currentAppState = newState
1944
+ activity.runOnUiThread {
1945
+ webView.evaluateJavascript(
1946
+ "window._craftAppStateCallback && window._craftAppStateCallback('$currentAppState')",
1947
+ null
1948
+ )
1949
+ }
1950
+ sendEvent("craftAppStateChange", mapOf("state" to currentAppState))
1951
+ }
1952
+ }
1953
+
1954
+ // ==================== Contacts ====================
1955
+
1956
+ @JavascriptInterface
1957
+ fun getContacts() {
1958
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS)
1959
+ != PackageManager.PERMISSION_GRANTED) {
1960
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CONTACTS), REQUEST_CONTACTS)
1961
+ activity.runOnUiThread {
1962
+ webView.evaluateJavascript("window._craftContactsReject && window._craftContactsReject('Permission denied')", null)
1963
+ }
1964
+ return
1965
+ }
1966
+
1967
+ val contacts = JSONArray()
1968
+ val cursor: Cursor? = activity.contentResolver.query(
1969
+ ContactsContract.Contacts.CONTENT_URI,
1970
+ null, null, null, ContactsContract.Contacts.DISPLAY_NAME + " ASC"
1971
+ )
1972
+
1973
+ cursor?.use {
1974
+ while (it.moveToNext()) {
1975
+ val id = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
1976
+ val name = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
1977
+
1978
+ val contact = JSONObject().apply {
1979
+ put("id", id)
1980
+ put("displayName", name ?: "")
1981
+ put("phoneNumbers", getContactPhones(id))
1982
+ put("emailAddresses", getContactEmails(id))
1983
+ }
1984
+ contacts.put(contact)
1985
+ }
1986
+ }
1987
+
1988
+ activity.runOnUiThread {
1989
+ webView.evaluateJavascript("window._craftContactsResolve && window._craftContactsResolve($contacts)", null)
1990
+ }
1991
+ }
1992
+
1993
+ private fun getContactPhones(contactId: String): JSONArray {
1994
+ val phones = JSONArray()
1995
+ val cursor = activity.contentResolver.query(
1996
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
1997
+ null,
1998
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
1999
+ arrayOf(contactId), null
2000
+ )
2001
+ cursor?.use {
2002
+ while (it.moveToNext()) {
2003
+ val phone = it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
2004
+ phones.put(phone)
2005
+ }
2006
+ }
2007
+ return phones
2008
+ }
2009
+
2010
+ private fun getContactEmails(contactId: String): JSONArray {
2011
+ val emails = JSONArray()
2012
+ val cursor = activity.contentResolver.query(
2013
+ ContactsContract.CommonDataKinds.Email.CONTENT_URI,
2014
+ null,
2015
+ ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?",
2016
+ arrayOf(contactId), null
2017
+ )
2018
+ cursor?.use {
2019
+ while (it.moveToNext()) {
2020
+ val email = it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS))
2021
+ emails.put(email)
2022
+ }
2023
+ }
2024
+ return emails
2025
+ }
2026
+
2027
+ @JavascriptInterface
2028
+ fun addContact(contactJson: String) {
2029
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CONTACTS)
2030
+ != PackageManager.PERMISSION_GRANTED) {
2031
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_CONTACTS), REQUEST_CONTACTS)
2032
+ activity.runOnUiThread {
2033
+ webView.evaluateJavascript("window._craftAddContactReject && window._craftAddContactReject('Permission denied')", null)
2034
+ }
2035
+ return
2036
+ }
2037
+
2038
+ try {
2039
+ val contact = JSONObject(contactJson)
2040
+ val ops = ArrayList<android.content.ContentProviderOperation>()
2041
+
2042
+ ops.add(android.content.ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
2043
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
2044
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
2045
+ .build())
2046
+
2047
+ // Name
2048
+ val displayName = contact.optString("displayName", "")
2049
+ if (displayName.isNotEmpty()) {
2050
+ ops.add(android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
2051
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
2052
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
2053
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName)
2054
+ .build())
2055
+ }
2056
+
2057
+ // Phone
2058
+ val phone = contact.optString("phone", "")
2059
+ if (phone.isNotEmpty()) {
2060
+ ops.add(android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
2061
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
2062
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
2063
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
2064
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
2065
+ .build())
2066
+ }
2067
+
2068
+ // Email
2069
+ val email = contact.optString("email", "")
2070
+ if (email.isNotEmpty()) {
2071
+ ops.add(android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
2072
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
2073
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
2074
+ .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
2075
+ .withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_HOME)
2076
+ .build())
2077
+ }
2078
+
2079
+ val results = activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
2080
+ val contactId = ContentUris.parseId(results[0].uri!!)
2081
+
2082
+ activity.runOnUiThread {
2083
+ webView.evaluateJavascript("window._craftAddContactResolve && window._craftAddContactResolve('$contactId')", null)
2084
+ }
2085
+ } catch (e: Exception) {
2086
+ activity.runOnUiThread {
2087
+ webView.evaluateJavascript("window._craftAddContactReject && window._craftAddContactReject('${e.message}')", null)
2088
+ }
2089
+ }
2090
+ }
2091
+
2092
+ // ==================== Calendar ====================
2093
+
2094
+ @JavascriptInterface
2095
+ fun getCalendarEvents(startDateMs: Long, endDateMs: Long) {
2096
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CALENDAR)
2097
+ != PackageManager.PERMISSION_GRANTED) {
2098
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CALENDAR), REQUEST_CALENDAR)
2099
+ activity.runOnUiThread {
2100
+ webView.evaluateJavascript("window._craftCalendarReject && window._craftCalendarReject('Permission denied')", null)
2101
+ }
2102
+ return
2103
+ }
2104
+
2105
+ val events = JSONArray()
2106
+ val startTime = if (startDateMs > 0) startDateMs else System.currentTimeMillis()
2107
+ val endTime = if (endDateMs > 0) endDateMs else startTime + (30L * 24 * 60 * 60 * 1000) // 30 days
2108
+
2109
+ val projection = arrayOf(
2110
+ CalendarContract.Events._ID,
2111
+ CalendarContract.Events.TITLE,
2112
+ CalendarContract.Events.DESCRIPTION,
2113
+ CalendarContract.Events.DTSTART,
2114
+ CalendarContract.Events.DTEND,
2115
+ CalendarContract.Events.EVENT_LOCATION,
2116
+ CalendarContract.Events.ALL_DAY
2117
+ )
2118
+
2119
+ val selection = "(${CalendarContract.Events.DTSTART} >= ?) AND (${CalendarContract.Events.DTSTART} <= ?)"
2120
+ val selectionArgs = arrayOf(startTime.toString(), endTime.toString())
2121
+
2122
+ val cursor = activity.contentResolver.query(
2123
+ CalendarContract.Events.CONTENT_URI,
2124
+ projection, selection, selectionArgs, CalendarContract.Events.DTSTART + " ASC"
2125
+ )
2126
+
2127
+ cursor?.use {
2128
+ while (it.moveToNext()) {
2129
+ val event = JSONObject().apply {
2130
+ put("id", it.getString(0))
2131
+ put("title", it.getString(1) ?: "")
2132
+ put("notes", it.getString(2) ?: "")
2133
+ put("startDate", it.getLong(3))
2134
+ put("endDate", it.getLong(4))
2135
+ put("location", it.getString(5) ?: "")
2136
+ put("isAllDay", it.getInt(6) == 1)
2137
+ }
2138
+ events.put(event)
2139
+ }
2140
+ }
2141
+
2142
+ activity.runOnUiThread {
2143
+ webView.evaluateJavascript("window._craftCalendarResolve && window._craftCalendarResolve($events)", null)
2144
+ }
2145
+ }
2146
+
2147
+ @JavascriptInterface
2148
+ fun createCalendarEvent(eventJson: String) {
2149
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CALENDAR)
2150
+ != PackageManager.PERMISSION_GRANTED) {
2151
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_CALENDAR), REQUEST_CALENDAR)
2152
+ activity.runOnUiThread {
2153
+ webView.evaluateJavascript("window._craftCreateEventReject && window._craftCreateEventReject('Permission denied')", null)
2154
+ }
2155
+ return
2156
+ }
2157
+
2158
+ try {
2159
+ val event = JSONObject(eventJson)
2160
+ val values = ContentValues().apply {
2161
+ put(CalendarContract.Events.TITLE, event.optString("title", ""))
2162
+ put(CalendarContract.Events.DESCRIPTION, event.optString("notes", ""))
2163
+ put(CalendarContract.Events.EVENT_LOCATION, event.optString("location", ""))
2164
+ put(CalendarContract.Events.DTSTART, event.optLong("startDate", System.currentTimeMillis()))
2165
+ put(CalendarContract.Events.DTEND, event.optLong("endDate", System.currentTimeMillis() + 3600000))
2166
+ put(CalendarContract.Events.ALL_DAY, if (event.optBoolean("isAllDay", false)) 1 else 0)
2167
+ put(CalendarContract.Events.CALENDAR_ID, 1)
2168
+ put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
2169
+ }
2170
+
2171
+ val uri = activity.contentResolver.insert(CalendarContract.Events.CONTENT_URI, values)
2172
+ val eventId = uri?.lastPathSegment ?: ""
2173
+
2174
+ activity.runOnUiThread {
2175
+ webView.evaluateJavascript("window._craftCreateEventResolve && window._craftCreateEventResolve('$eventId')", null)
2176
+ }
2177
+ } catch (e: Exception) {
2178
+ activity.runOnUiThread {
2179
+ webView.evaluateJavascript("window._craftCreateEventReject && window._craftCreateEventReject('${e.message}')", null)
2180
+ }
2181
+ }
2182
+ }
2183
+
2184
+ @JavascriptInterface
2185
+ fun deleteCalendarEvent(eventId: String) {
2186
+ try {
2187
+ val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId.toLong())
2188
+ activity.contentResolver.delete(uri, null, null)
2189
+ activity.runOnUiThread {
2190
+ webView.evaluateJavascript("window._craftDeleteEventResolve && window._craftDeleteEventResolve(true)", null)
2191
+ }
2192
+ } catch (e: Exception) {
2193
+ activity.runOnUiThread {
2194
+ webView.evaluateJavascript("window._craftDeleteEventReject && window._craftDeleteEventReject('${e.message}')", null)
2195
+ }
2196
+ }
2197
+ }
2198
+
2199
+ // ==================== Local Notifications ====================
2200
+
2201
+ @JavascriptInterface
2202
+ fun scheduleNotification(notificationJson: String) {
2203
+ try {
2204
+ val notif = JSONObject(notificationJson)
2205
+ val title = notif.optString("title", "")
2206
+ val body = notif.optString("body", "")
2207
+ val id = notif.optString("id", System.currentTimeMillis().toString())
2208
+
2209
+ createNotificationChannel()
2210
+
2211
+ val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2212
+
2213
+ val builder = androidx.core.app.NotificationCompat.Builder(activity, notificationChannelId)
2214
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
2215
+ .setContentTitle(title)
2216
+ .setContentText(body)
2217
+ .setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT)
2218
+ .setAutoCancel(true)
2219
+
2220
+ // Schedule with delay if specified
2221
+ val delay = notif.optLong("delay", 0)
2222
+ if (delay > 0) {
2223
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
2224
+ notificationManager.notify(id.hashCode(), builder.build())
2225
+ }, delay)
2226
+ } else {
2227
+ notificationManager.notify(id.hashCode(), builder.build())
2228
+ }
2229
+
2230
+ activity.runOnUiThread {
2231
+ webView.evaluateJavascript("window._craftNotifResolve && window._craftNotifResolve('$id')", null)
2232
+ }
2233
+ } catch (e: Exception) {
2234
+ activity.runOnUiThread {
2235
+ webView.evaluateJavascript("window._craftNotifReject && window._craftNotifReject('${e.message}')", null)
2236
+ }
2237
+ }
2238
+ }
2239
+
2240
+ private fun createNotificationChannel() {
2241
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
2242
+ val name = "Craft Notifications"
2243
+ val descriptionText = "Notifications from Craft app"
2244
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
2245
+ val channel = NotificationChannel(notificationChannelId, name, importance).apply {
2246
+ description = descriptionText
2247
+ }
2248
+ val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2249
+ notificationManager.createNotificationChannel(channel)
2250
+ }
2251
+ }
2252
+
2253
+ @JavascriptInterface
2254
+ fun cancelNotification(id: String) {
2255
+ val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2256
+ notificationManager.cancel(id.hashCode())
2257
+ }
2258
+
2259
+ @JavascriptInterface
2260
+ fun cancelAllNotifications() {
2261
+ val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2262
+ notificationManager.cancelAll()
2263
+ }
2264
+
2265
+ // ==================== In-App Purchase ====================
2266
+
2267
+ @JavascriptInterface
2268
+ fun getProducts(productIdsJson: String) {
2269
+ try {
2270
+ val productIds = JSONArray(productIdsJson)
2271
+ val productList = mutableListOf<String>()
2272
+ for (i in 0 until productIds.length()) {
2273
+ productList.add(productIds.getString(i))
2274
+ }
2275
+
2276
+ billingClient = BillingClient.newBuilder(activity)
2277
+ .setListener { _, _ -> }
2278
+ .enablePendingPurchases()
2279
+ .build()
2280
+
2281
+ billingClient?.startConnection(object : BillingClientStateListener {
2282
+ override fun onBillingSetupFinished(billingResult: BillingResult) {
2283
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
2284
+ val params = QueryProductDetailsParams.newBuilder()
2285
+ .setProductList(productList.map { productId ->
2286
+ QueryProductDetailsParams.Product.newBuilder()
2287
+ .setProductId(productId)
2288
+ .setProductType(BillingClient.ProductType.INAPP)
2289
+ .build()
2290
+ })
2291
+ .build()
2292
+
2293
+ billingClient?.queryProductDetailsAsync(params) { result, productDetailsList ->
2294
+ if (result.responseCode == BillingClient.BillingResponseCode.OK) {
2295
+ val products = JSONArray()
2296
+ productDetailsList.forEach { details ->
2297
+ products.put(JSONObject().apply {
2298
+ put("id", details.productId)
2299
+ put("displayName", details.name)
2300
+ put("description", details.description)
2301
+ put("price", details.oneTimePurchaseOfferDetails?.formattedPrice ?: "")
2302
+ })
2303
+ }
2304
+ activity.runOnUiThread {
2305
+ webView.evaluateJavascript("window._craftProductsResolve && window._craftProductsResolve($products)", null)
2306
+ }
2307
+ } else {
2308
+ activity.runOnUiThread {
2309
+ webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('Query failed')", null)
2310
+ }
2311
+ }
2312
+ }
2313
+ }
2314
+ }
2315
+
2316
+ override fun onBillingServiceDisconnected() {
2317
+ activity.runOnUiThread {
2318
+ webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('Billing disconnected')", null)
2319
+ }
2320
+ }
2321
+ })
2322
+ } catch (e: Exception) {
2323
+ activity.runOnUiThread {
2324
+ webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('${e.message}')", null)
2325
+ }
2326
+ }
2327
+ }
2328
+
2329
+ @JavascriptInterface
2330
+ fun purchase(productId: String) {
2331
+ // Simplified - real implementation would query product details first
2332
+ activity.runOnUiThread {
2333
+ webView.evaluateJavascript("window._craftPurchaseReject && window._craftPurchaseReject('Purchase flow not fully implemented')", null)
2334
+ }
2335
+ }
2336
+
2337
+ @JavascriptInterface
2338
+ fun restorePurchases() {
2339
+ billingClient?.queryPurchasesAsync(
2340
+ QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build()
2341
+ ) { result, purchases ->
2342
+ if (result.responseCode == BillingClient.BillingResponseCode.OK) {
2343
+ val restored = JSONArray()
2344
+ purchases.forEach { purchase ->
2345
+ restored.put(JSONObject().apply {
2346
+ put("productId", purchase.products.firstOrNull())
2347
+ put("orderId", purchase.orderId)
2348
+ })
2349
+ }
2350
+ activity.runOnUiThread {
2351
+ webView.evaluateJavascript("window._craftRestoreResolve && window._craftRestoreResolve($restored)", null)
2352
+ }
2353
+ } else {
2354
+ activity.runOnUiThread {
2355
+ webView.evaluateJavascript("window._craftRestoreReject && window._craftRestoreReject('Restore failed')", null)
2356
+ }
2357
+ }
2358
+ }
2359
+ }
2360
+
2361
+ // ==================== Keep Awake ====================
2362
+
2363
+ @JavascriptInterface
2364
+ fun setKeepAwake(enabled: Boolean): Boolean {
2365
+ activity.runOnUiThread {
2366
+ if (enabled) {
2367
+ activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
2368
+ } else {
2369
+ activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
2370
+ }
2371
+ isKeepingAwake = enabled
2372
+ }
2373
+ return true
2374
+ }
2375
+
2376
+ // ==================== Orientation Lock ====================
2377
+
2378
+ @JavascriptInterface
2379
+ fun lockOrientation(orientation: String): Boolean {
2380
+ activity.runOnUiThread {
2381
+ activity.requestedOrientation = when (orientation) {
2382
+ "portrait" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
2383
+ "landscape" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
2384
+ "landscapeLeft" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
2385
+ "landscapeRight" -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
2386
+ else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
2387
+ }
2388
+ }
2389
+ return true
2390
+ }
2391
+
2392
+ @JavascriptInterface
2393
+ fun unlockOrientation(): Boolean {
2394
+ activity.runOnUiThread {
2395
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
2396
+ }
2397
+ return true
2398
+ }
2399
+
2400
+ // ==================== Deep Links ====================
2401
+
2402
+ fun handleDeepLink(uri: android.net.Uri) {
2403
+ sendEvent("craftDeepLink", mapOf(
2404
+ "url" to uri.toString(),
2405
+ "scheme" to (uri.scheme ?: ""),
2406
+ "host" to (uri.host ?: ""),
2407
+ "path" to (uri.path ?: ""),
2408
+ "query" to (uri.query ?: "")
2409
+ ))
2410
+ }
2411
+
2412
+ // ==================== QR/Barcode Scanner ====================
2413
+
2414
+ @JavascriptInterface
2415
+ fun scanQRCode() {
2416
+ // Uses ML Kit barcode scanning - requires camera permission
2417
+ activity.runOnUiThread {
2418
+ webView.evaluateJavascript(
2419
+ "window._craftQRReject && window._craftQRReject('QR scanning requires camera integration - use native camera intent')",
2420
+ null
2421
+ )
2422
+ }
2423
+ }
2424
+
2425
+ // ==================== File Picker ====================
2426
+
2427
+ @JavascriptInterface
2428
+ fun pickFile(typesJson: String) {
2429
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
2430
+ addCategory(Intent.CATEGORY_OPENABLE)
2431
+ type = "*/*"
2432
+ }
2433
+ activity.startActivityForResult(intent, REQUEST_FILE_PICKER)
2434
+ }
2435
+
2436
+ // ==================== File Download ====================
2437
+
2438
+ @JavascriptInterface
2439
+ fun downloadFile(url: String, filename: String) {
2440
+ try {
2441
+ val request = DownloadManager.Request(Uri.parse(url)).apply {
2442
+ setTitle(filename)
2443
+ setDescription("Downloading...")
2444
+ setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
2445
+ setDestinationInExternalPublicDir(android.os.Environment.DIRECTORY_DOWNLOADS, filename)
2446
+ }
2447
+
2448
+ val downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
2449
+ val downloadId = downloadManager.enqueue(request)
2450
+
2451
+ activity.runOnUiThread {
2452
+ webView.evaluateJavascript(
2453
+ "window._craftDownloadResolve && window._craftDownloadResolve('$downloadId')",
2454
+ null
2455
+ )
2456
+ }
2457
+ } catch (e: Exception) {
2458
+ activity.runOnUiThread {
2459
+ webView.evaluateJavascript(
2460
+ "window._craftDownloadReject && window._craftDownloadReject('${e.message}')",
2461
+ null
2462
+ )
2463
+ }
2464
+ }
2465
+ }
2466
+
2467
+ @JavascriptInterface
2468
+ fun saveFile(data: String, filename: String) {
2469
+ try {
2470
+ val documentsDir = activity.getExternalFilesDir(android.os.Environment.DIRECTORY_DOCUMENTS)
2471
+ val file = File(documentsDir, filename)
2472
+
2473
+ if (data.startsWith("data:")) {
2474
+ // Base64 data URL
2475
+ val parts = data.split(",")
2476
+ if (parts.size == 2) {
2477
+ val bytes = Base64.decode(parts[1], Base64.DEFAULT)
2478
+ file.writeBytes(bytes)
2479
+ }
2480
+ } else {
2481
+ file.writeText(data)
2482
+ }
2483
+
2484
+ activity.runOnUiThread {
2485
+ webView.evaluateJavascript(
2486
+ "window._craftSaveResolve && window._craftSaveResolve('${file.absolutePath}')",
2487
+ null
2488
+ )
2489
+ }
2490
+ } catch (e: Exception) {
2491
+ activity.runOnUiThread {
2492
+ webView.evaluateJavascript(
2493
+ "window._craftSaveReject && window._craftSaveReject('${e.message}')",
2494
+ null
2495
+ )
2496
+ }
2497
+ }
2498
+ }
2499
+
2500
+ // ==================== Google Sign In ====================
2501
+
2502
+ @JavascriptInterface
2503
+ fun signInWithGoogle() {
2504
+ // Google Sign In requires configuration in build.gradle
2505
+ activity.runOnUiThread {
2506
+ webView.evaluateJavascript(
2507
+ "window._craftGoogleReject && window._craftGoogleReject('Google Sign In requires app configuration')",
2508
+ null
2509
+ )
2510
+ }
2511
+ }
2512
+
2513
+ // ==================== Audio Recording ====================
2514
+
2515
+ @JavascriptInterface
2516
+ fun startAudioRecording() {
2517
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
2518
+ != PackageManager.PERMISSION_GRANTED) {
2519
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO), REQUEST_AUDIO)
2520
+ activity.runOnUiThread {
2521
+ webView.evaluateJavascript("window._craftAudioReject && window._craftAudioReject('Permission denied')", null)
2522
+ }
2523
+ return
2524
+ }
2525
+
2526
+ try {
2527
+ val cacheDir = activity.cacheDir
2528
+ audioFile = File.createTempFile("recording_", ".m4a", cacheDir)
2529
+
2530
+ mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
2531
+ MediaRecorder(activity)
2532
+ } else {
2533
+ @Suppress("DEPRECATION")
2534
+ MediaRecorder()
2535
+ }
2536
+
2537
+ mediaRecorder?.apply {
2538
+ setAudioSource(MediaRecorder.AudioSource.MIC)
2539
+ setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
2540
+ setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
2541
+ setOutputFile(audioFile?.absolutePath)
2542
+ prepare()
2543
+ start()
2544
+ }
2545
+
2546
+ activity.runOnUiThread {
2547
+ webView.evaluateJavascript("window._craftAudioResolve && window._craftAudioResolve(true)", null)
2548
+ }
2549
+ } catch (e: Exception) {
2550
+ activity.runOnUiThread {
2551
+ webView.evaluateJavascript("window._craftAudioReject && window._craftAudioReject('${e.message}')", null)
2552
+ }
2553
+ }
2554
+ }
2555
+
2556
+ @JavascriptInterface
2557
+ fun stopAudioRecording() {
2558
+ try {
2559
+ mediaRecorder?.stop()
2560
+ mediaRecorder?.release()
2561
+ mediaRecorder = null
2562
+
2563
+ audioFile?.let { file ->
2564
+ val bytes = file.readBytes()
2565
+ val base64 = "data:audio/m4a;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
2566
+ activity.runOnUiThread {
2567
+ webView.evaluateJavascript("window._craftAudioStopResolve && window._craftAudioStopResolve('$base64')", null)
2568
+ }
2569
+ } ?: run {
2570
+ activity.runOnUiThread {
2571
+ webView.evaluateJavascript("window._craftAudioStopReject && window._craftAudioStopReject('No recording')", null)
2572
+ }
2573
+ }
2574
+ } catch (e: Exception) {
2575
+ activity.runOnUiThread {
2576
+ webView.evaluateJavascript("window._craftAudioStopReject && window._craftAudioStopReject('${e.message}')", null)
2577
+ }
2578
+ }
2579
+ }
2580
+
2581
+ // ==================== Video Recording ====================
2582
+
2583
+ @JavascriptInterface
2584
+ fun startVideoRecording() {
2585
+ val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply {
2586
+ putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1)
2587
+ }
2588
+ activity.startActivityForResult(intent, REQUEST_VIDEO)
2589
+ }
2590
+
2591
+ // ==================== Motion Sensors ====================
2592
+
2593
+ @JavascriptInterface
2594
+ fun startMotionUpdates(intervalMs: Int): Boolean {
2595
+ sensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
2596
+ accelerometer = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
2597
+ gyroscope = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
2598
+
2599
+ sensorListener = object : SensorEventListener {
2600
+ private var lastAccel: FloatArray? = null
2601
+ private var lastGyro: FloatArray? = null
2602
+
2603
+ override fun onSensorChanged(event: SensorEvent) {
2604
+ when (event.sensor.type) {
2605
+ Sensor.TYPE_ACCELEROMETER -> lastAccel = event.values.clone()
2606
+ Sensor.TYPE_GYROSCOPE -> lastGyro = event.values.clone()
2607
+ }
2608
+
2609
+ val accel = lastAccel ?: floatArrayOf(0f, 0f, 0f)
2610
+ val gyro = lastGyro ?: floatArrayOf(0f, 0f, 0f)
2611
+
2612
+ sendEvent("craftMotionUpdate", mapOf(
2613
+ "acceleration" to mapOf("x" to accel[0], "y" to accel[1], "z" to accel[2]),
2614
+ "rotation" to mapOf("alpha" to gyro[0], "beta" to gyro[1], "gamma" to gyro[2])
2615
+ ))
2616
+ }
2617
+
2618
+ override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
2619
+ }
2620
+
2621
+ val delay = when {
2622
+ intervalMs <= 20 -> SensorManager.SENSOR_DELAY_FASTEST
2623
+ intervalMs <= 60 -> SensorManager.SENSOR_DELAY_GAME
2624
+ intervalMs <= 200 -> SensorManager.SENSOR_DELAY_UI
2625
+ else -> SensorManager.SENSOR_DELAY_NORMAL
2626
+ }
2627
+
2628
+ accelerometer?.let { sensorManager?.registerListener(sensorListener, it, delay) }
2629
+ gyroscope?.let { sensorManager?.registerListener(sensorListener, it, delay) }
2630
+
2631
+ return true
2632
+ }
2633
+
2634
+ @JavascriptInterface
2635
+ fun stopMotionUpdates() {
2636
+ sensorListener?.let { sensorManager?.unregisterListener(it) }
2637
+ sensorListener = null
2638
+ }
2639
+
2640
+ // ==================== Local Database ====================
2641
+
2642
+ @JavascriptInterface
2643
+ fun dbExecute(sql: String, paramsJson: String) {
2644
+ try {
2645
+ if (database == null) {
2646
+ database = activity.openOrCreateDatabase("craft.db", Context.MODE_PRIVATE, null)
2647
+ }
2648
+
2649
+ val params = JSONArray(paramsJson)
2650
+ val args = Array(params.length()) { params.getString(it) }
2651
+
2652
+ database?.execSQL(sql, args)
2653
+
2654
+ activity.runOnUiThread {
2655
+ webView.evaluateJavascript(
2656
+ "window._craftDbExecResolve && window._craftDbExecResolve({rowsAffected: 1})",
2657
+ null
2658
+ )
2659
+ }
2660
+ } catch (e: Exception) {
2661
+ activity.runOnUiThread {
2662
+ webView.evaluateJavascript(
2663
+ "window._craftDbExecReject && window._craftDbExecReject('${e.message}')",
2664
+ null
2665
+ )
2666
+ }
2667
+ }
2668
+ }
2669
+
2670
+ @JavascriptInterface
2671
+ fun dbQuery(sql: String, paramsJson: String) {
2672
+ try {
2673
+ if (database == null) {
2674
+ database = activity.openOrCreateDatabase("craft.db", Context.MODE_PRIVATE, null)
2675
+ }
2676
+
2677
+ val params = JSONArray(paramsJson)
2678
+ val args = Array(params.length()) { params.getString(it) }
2679
+
2680
+ val cursor = database?.rawQuery(sql, args)
2681
+ val results = JSONArray()
2682
+
2683
+ cursor?.use {
2684
+ val columns = it.columnNames
2685
+ while (it.moveToNext()) {
2686
+ val row = JSONObject()
2687
+ columns.forEachIndexed { index, col ->
2688
+ row.put(col, it.getString(index))
2689
+ }
2690
+ results.put(row)
2691
+ }
2692
+ }
2693
+
2694
+ activity.runOnUiThread {
2695
+ webView.evaluateJavascript(
2696
+ "window._craftDbQueryResolve && window._craftDbQueryResolve($results)",
2697
+ null
2698
+ )
2699
+ }
2700
+ } catch (e: Exception) {
2701
+ activity.runOnUiThread {
2702
+ webView.evaluateJavascript(
2703
+ "window._craftDbQueryReject && window._craftDbQueryReject('${e.message}')",
2704
+ null
2705
+ )
2706
+ }
2707
+ }
2708
+ }
2709
+
2710
+ // ==================== Bluetooth ====================
2711
+
2712
+ @JavascriptInterface
2713
+ fun startBluetoothScan() {
2714
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_SCAN)
2715
+ != PackageManager.PERMISSION_GRANTED) {
2716
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
2717
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.BLUETOOTH_SCAN), REQUEST_BLUETOOTH)
2718
+ }
2719
+ activity.runOnUiThread {
2720
+ webView.evaluateJavascript("window._craftBleReject && window._craftBleReject('Permission denied')", null)
2721
+ }
2722
+ return
2723
+ }
2724
+
2725
+ val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
2726
+ bluetoothAdapter = bluetoothManager.adapter
2727
+ bluetoothScanner = bluetoothAdapter?.bluetoothLeScanner
2728
+
2729
+ bleScanCallback = object : ScanCallback() {
2730
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
2731
+ sendEvent("craftBluetoothDevice", mapOf(
2732
+ "id" to result.device.address,
2733
+ "name" to (result.device.name ?: "Unknown"),
2734
+ "rssi" to result.rssi
2735
+ ))
2736
+ }
2737
+ }
2738
+
2739
+ bluetoothScanner?.startScan(bleScanCallback)
2740
+ activity.runOnUiThread {
2741
+ webView.evaluateJavascript("window._craftBleResolve && window._craftBleResolve(true)", null)
2742
+ }
2743
+ }
2744
+
2745
+ @JavascriptInterface
2746
+ fun stopBluetoothScan() {
2747
+ bleScanCallback?.let { bluetoothScanner?.stopScan(it) }
2748
+ bleScanCallback = null
2749
+ }
2750
+
2751
+ // ==================== NFC ====================
2752
+
2753
+ @JavascriptInterface
2754
+ fun scanNFC() {
2755
+ val nfcAdapter = NfcAdapter.getDefaultAdapter(activity)
2756
+ if (nfcAdapter == null || !nfcAdapter.isEnabled) {
2757
+ activity.runOnUiThread {
2758
+ webView.evaluateJavascript("window._craftNfcReject && window._craftNfcReject('NFC not available')", null)
2759
+ }
2760
+ return
2761
+ }
2762
+ // NFC requires foreground dispatch - simplified implementation
2763
+ activity.runOnUiThread {
2764
+ webView.evaluateJavascript("window._craftNfcReject && window._craftNfcReject('NFC requires activity integration')", null)
2765
+ }
2766
+ }
2767
+
2768
+ // ==================== Fitness ====================
2769
+
2770
+ @JavascriptInterface
2771
+ fun requestFitnessAuthorization(typesJson: String) {
2772
+ // Google Fit requires configuration
2773
+ activity.runOnUiThread {
2774
+ webView.evaluateJavascript("window._craftFitnessAuthReject && window._craftFitnessAuthReject('Fitness requires Google Fit configuration')", null)
2775
+ }
2776
+ }
2777
+
2778
+ @JavascriptInterface
2779
+ fun getFitnessData(type: String, startDate: Long, endDate: Long) {
2780
+ activity.runOnUiThread {
2781
+ webView.evaluateJavascript("window._craftFitnessDataReject && window._craftFitnessDataReject('Fitness requires Google Fit configuration')", null)
2782
+ }
2783
+ }
2784
+
2785
+ // ==================== Screen Capture ====================
2786
+
2787
+ @JavascriptInterface
2788
+ fun takeScreenshot() {
2789
+ activity.runOnUiThread {
2790
+ try {
2791
+ val view = webView.rootView
2792
+ view.isDrawingCacheEnabled = true
2793
+ view.buildDrawingCache()
2794
+ val bitmap = Bitmap.createBitmap(view.drawingCache)
2795
+ view.isDrawingCacheEnabled = false
2796
+
2797
+ val outputStream = ByteArrayOutputStream()
2798
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
2799
+ val base64 = "data:image/png;base64," + Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
2800
+
2801
+ webView.evaluateJavascript("window._craftScreenshotResolve && window._craftScreenshotResolve('$base64')", null)
2802
+ } catch (e: Exception) {
2803
+ webView.evaluateJavascript("window._craftScreenshotReject && window._craftScreenshotReject('${e.message}')", null)
2804
+ }
2805
+ }
2806
+ }
2807
+
2808
+ // ==================== Background Tasks ====================
2809
+
2810
+ @JavascriptInterface
2811
+ fun registerBackgroundTask(taskId: String) {
2812
+ try {
2813
+ // WorkManager tasks don't require explicit registration, just track them
2814
+ activity.runOnUiThread {
2815
+ webView.evaluateJavascript(
2816
+ "window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', registered: true})",
2817
+ null
2818
+ )
2819
+ }
2820
+ } catch (e: Exception) {
2821
+ activity.runOnUiThread {
2822
+ webView.evaluateJavascript(
2823
+ "window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
2824
+ null
2825
+ )
2826
+ }
2827
+ }
2828
+ }
2829
+
2830
+ @JavascriptInterface
2831
+ fun scheduleBackgroundTask(taskId: String, delay: Long, requiresNetwork: Boolean, requiresCharging: Boolean) {
2832
+ try {
2833
+ // Note: Full WorkManager implementation requires adding the dependency
2834
+ // implementation 'androidx.work:work-runtime-ktx:2.9.0'
2835
+ // This is a placeholder that shows the API structure
2836
+ activity.runOnUiThread {
2837
+ webView.evaluateJavascript(
2838
+ "window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', scheduled: true, delay: $delay})",
2839
+ null
2840
+ )
2841
+ }
2842
+ } catch (e: Exception) {
2843
+ activity.runOnUiThread {
2844
+ webView.evaluateJavascript(
2845
+ "window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
2846
+ null
2847
+ )
2848
+ }
2849
+ }
2850
+ }
2851
+
2852
+ @JavascriptInterface
2853
+ fun cancelBackgroundTask(taskId: String) {
2854
+ try {
2855
+ activity.runOnUiThread {
2856
+ webView.evaluateJavascript(
2857
+ "window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', cancelled: true})",
2858
+ null
2859
+ )
2860
+ }
2861
+ } catch (e: Exception) {
2862
+ activity.runOnUiThread {
2863
+ webView.evaluateJavascript(
2864
+ "window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
2865
+ null
2866
+ )
2867
+ }
2868
+ }
2869
+ }
2870
+
2871
+ @JavascriptInterface
2872
+ fun cancelAllBackgroundTasks() {
2873
+ try {
2874
+ activity.runOnUiThread {
2875
+ webView.evaluateJavascript(
2876
+ "window._craftBgTaskResolve && window._craftBgTaskResolve({cancelled: true})",
2877
+ null
2878
+ )
2879
+ }
2880
+ } catch (e: Exception) {
2881
+ activity.runOnUiThread {
2882
+ webView.evaluateJavascript(
2883
+ "window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
2884
+ null
2885
+ )
2886
+ }
2887
+ }
2888
+ }
2889
+
2890
+ // ==================== PDF Viewer ====================
2891
+
2892
+ @JavascriptInterface
2893
+ fun openPDF(source: String, page: Int) {
2894
+ activity.runOnUiThread {
2895
+ try {
2896
+ val uri = if (source.startsWith("data:")) {
2897
+ // Save base64 to temp file and open
2898
+ val base64Data = source.substringAfter(",")
2899
+ val bytes = Base64.decode(base64Data, Base64.DEFAULT)
2900
+ val tempFile = File.createTempFile("craft_pdf_", ".pdf", activity.cacheDir)
2901
+ tempFile.writeBytes(bytes)
2902
+ Uri.fromFile(tempFile)
2903
+ } else {
2904
+ Uri.parse(source)
2905
+ }
2906
+
2907
+ val intent = Intent(Intent.ACTION_VIEW).apply {
2908
+ setDataAndType(uri, "application/pdf")
2909
+ addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
2910
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
2911
+ }
2912
+
2913
+ activity.startActivity(intent)
2914
+ webView.evaluateJavascript(
2915
+ "window._craftPDFResolve && window._craftPDFResolve({opened: true})",
2916
+ null
2917
+ )
2918
+ } catch (e: Exception) {
2919
+ webView.evaluateJavascript(
2920
+ "window._craftPDFReject && window._craftPDFReject('${e.message}')",
2921
+ null
2922
+ )
2923
+ }
2924
+ }
2925
+ }
2926
+
2927
+ @JavascriptInterface
2928
+ fun closePDF() {
2929
+ // PDFs are opened in external viewers, so we just acknowledge
2930
+ activity.runOnUiThread {
2931
+ webView.evaluateJavascript(
2932
+ "window._craftPDFResolve && window._craftPDFResolve(true)",
2933
+ null
2934
+ )
2935
+ }
2936
+ }
2937
+
2938
+ // ==================== Contacts Picker ====================
2939
+
2940
+ @JavascriptInterface
2941
+ fun pickContact(multiple: Boolean) {
2942
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS)
2943
+ != PackageManager.PERMISSION_GRANTED) {
2944
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CONTACTS), REQUEST_PICK_CONTACT)
2945
+ return
2946
+ }
2947
+
2948
+ activity.runOnUiThread {
2949
+ val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
2950
+ activity.startActivityForResult(intent, REQUEST_PICK_CONTACT)
2951
+ }
2952
+ }
2953
+
2954
+ fun handleContactPickerResult(resultCode: Int, data: Intent?) {
2955
+ if (resultCode != Activity.RESULT_OK || data?.data == null) {
2956
+ activity.runOnUiThread {
2957
+ webView.evaluateJavascript(
2958
+ "window._craftPickContactReject && window._craftPickContactReject('Cancelled')",
2959
+ null
2960
+ )
2961
+ }
2962
+ return
2963
+ }
2964
+
2965
+ try {
2966
+ val contactUri = data.data!!
2967
+ val projection = arrayOf(
2968
+ ContactsContract.Contacts._ID,
2969
+ ContactsContract.Contacts.DISPLAY_NAME
2970
+ )
2971
+
2972
+ activity.contentResolver.query(contactUri, projection, null, null, null)?.use { cursor ->
2973
+ if (cursor.moveToFirst()) {
2974
+ val contactId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
2975
+ val displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
2976
+
2977
+ // Get phone numbers
2978
+ val phones = mutableListOf<Map<String, String>>()
2979
+ activity.contentResolver.query(
2980
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
2981
+ arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.TYPE),
2982
+ "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
2983
+ arrayOf(contactId),
2984
+ null
2985
+ )?.use { phoneCursor ->
2986
+ while (phoneCursor.moveToNext()) {
2987
+ val number = phoneCursor.getString(phoneCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
2988
+ phones.add(mapOf("number" to number))
2989
+ }
2990
+ }
2991
+
2992
+ // Get emails
2993
+ val emails = mutableListOf<Map<String, String>>()
2994
+ activity.contentResolver.query(
2995
+ ContactsContract.CommonDataKinds.Email.CONTENT_URI,
2996
+ arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS),
2997
+ "${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?",
2998
+ arrayOf(contactId),
2999
+ null
3000
+ )?.use { emailCursor ->
3001
+ while (emailCursor.moveToNext()) {
3002
+ val address = emailCursor.getString(emailCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS))
3003
+ emails.add(mapOf("address" to address))
3004
+ }
3005
+ }
3006
+
3007
+ val result = JSONObject().apply {
3008
+ put("id", contactId)
3009
+ put("displayName", displayName)
3010
+ put("phoneNumbers", JSONArray(phones.map { JSONObject(it) }))
3011
+ put("emailAddresses", JSONArray(emails.map { JSONObject(it) }))
3012
+ }
3013
+
3014
+ activity.runOnUiThread {
3015
+ webView.evaluateJavascript(
3016
+ "window._craftPickContactResolve && window._craftPickContactResolve($result)",
3017
+ null
3018
+ )
3019
+ }
3020
+ }
3021
+ }
3022
+ } catch (e: Exception) {
3023
+ activity.runOnUiThread {
3024
+ webView.evaluateJavascript(
3025
+ "window._craftPickContactReject && window._craftPickContactReject('${e.message}')",
3026
+ null
3027
+ )
3028
+ }
3029
+ }
3030
+ }
3031
+
3032
+ // ==================== App Shortcuts ====================
3033
+
3034
+ @JavascriptInterface
3035
+ fun setShortcuts(shortcutsJson: String) {
3036
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
3037
+ activity.runOnUiThread {
3038
+ webView.evaluateJavascript(
3039
+ "window._craftShortcutsReject && window._craftShortcutsReject('App shortcuts require Android 7.1+')",
3040
+ null
3041
+ )
3042
+ }
3043
+ return
3044
+ }
3045
+
3046
+ try {
3047
+ val shortcutManager = activity.getSystemService(android.content.pm.ShortcutManager::class.java)
3048
+ val shortcuts = mutableListOf<android.content.pm.ShortcutInfo>()
3049
+ val jsonArray = JSONArray(shortcutsJson)
3050
+
3051
+ for (i in 0 until jsonArray.length()) {
3052
+ val shortcut = jsonArray.getJSONObject(i)
3053
+ val type = shortcut.getString("type")
3054
+ val title = shortcut.getString("title")
3055
+ val subtitle = if (shortcut.has("subtitle")) shortcut.getString("subtitle") else null
3056
+
3057
+ val intent = Intent(activity, activity::class.java).apply {
3058
+ action = Intent.ACTION_VIEW
3059
+ putExtra("shortcut_type", type)
3060
+ }
3061
+
3062
+ val builder = android.content.pm.ShortcutInfo.Builder(activity, type)
3063
+ .setShortLabel(title)
3064
+ .setIntent(intent)
3065
+
3066
+ subtitle?.let { builder.setLongLabel(it) }
3067
+
3068
+ shortcuts.add(builder.build())
3069
+ }
3070
+
3071
+ shortcutManager?.dynamicShortcuts = shortcuts
3072
+
3073
+ activity.runOnUiThread {
3074
+ webView.evaluateJavascript(
3075
+ "window._craftShortcutsResolve && window._craftShortcutsResolve({count: ${shortcuts.size}})",
3076
+ null
3077
+ )
3078
+ }
3079
+ } catch (e: Exception) {
3080
+ activity.runOnUiThread {
3081
+ webView.evaluateJavascript(
3082
+ "window._craftShortcutsReject && window._craftShortcutsReject('${e.message}')",
3083
+ null
3084
+ )
3085
+ }
3086
+ }
3087
+ }
3088
+
3089
+ @JavascriptInterface
3090
+ fun clearShortcuts() {
3091
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
3092
+ try {
3093
+ val shortcutManager = activity.getSystemService(android.content.pm.ShortcutManager::class.java)
3094
+ shortcutManager?.removeAllDynamicShortcuts()
3095
+
3096
+ activity.runOnUiThread {
3097
+ webView.evaluateJavascript(
3098
+ "window._craftShortcutsResolve && window._craftShortcutsResolve(true)",
3099
+ null
3100
+ )
3101
+ }
3102
+ } catch (e: Exception) {
3103
+ activity.runOnUiThread {
3104
+ webView.evaluateJavascript(
3105
+ "window._craftShortcutsReject && window._craftShortcutsReject('${e.message}')",
3106
+ null
3107
+ )
3108
+ }
3109
+ }
3110
+ } else {
3111
+ activity.runOnUiThread {
3112
+ webView.evaluateJavascript(
3113
+ "window._craftShortcutsResolve && window._craftShortcutsResolve(true)",
3114
+ null
3115
+ )
3116
+ }
3117
+ }
3118
+ }
3119
+
3120
+ // ==================== Shared Preferences (Keychain equivalent) ====================
3121
+
3122
+ @JavascriptInterface
3123
+ fun setSharedItem(key: String, value: String, group: String) {
3124
+ try {
3125
+ val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
3126
+ val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
3127
+ prefs.edit().putString(key, value).apply()
3128
+
3129
+ activity.runOnUiThread {
3130
+ webView.evaluateJavascript(
3131
+ "window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({success: true, key: '$key'})",
3132
+ null
3133
+ )
3134
+ }
3135
+ } catch (e: Exception) {
3136
+ activity.runOnUiThread {
3137
+ webView.evaluateJavascript(
3138
+ "window._craftSharedKeychainReject && window._craftSharedKeychainReject('${e.message}')",
3139
+ null
3140
+ )
3141
+ }
3142
+ }
3143
+ }
3144
+
3145
+ @JavascriptInterface
3146
+ fun getSharedItem(key: String, group: String) {
3147
+ try {
3148
+ val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
3149
+ val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
3150
+ val value = prefs.getString(key, null)
3151
+
3152
+ activity.runOnUiThread {
3153
+ if (value != null) {
3154
+ webView.evaluateJavascript(
3155
+ "window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({value: '$value', key: '$key'})",
3156
+ null
3157
+ )
3158
+ } else {
3159
+ webView.evaluateJavascript(
3160
+ "window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({value: null, key: '$key'})",
3161
+ null
3162
+ )
3163
+ }
3164
+ }
3165
+ } catch (e: Exception) {
3166
+ activity.runOnUiThread {
3167
+ webView.evaluateJavascript(
3168
+ "window._craftSharedKeychainReject && window._craftSharedKeychainReject('${e.message}')",
3169
+ null
3170
+ )
3171
+ }
3172
+ }
3173
+ }
3174
+
3175
+ @JavascriptInterface
3176
+ fun removeSharedItem(key: String, group: String) {
3177
+ try {
3178
+ val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
3179
+ val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
3180
+ prefs.edit().remove(key).apply()
3181
+
3182
+ activity.runOnUiThread {
3183
+ webView.evaluateJavascript(
3184
+ "window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({success: true, key: '$key'})",
3185
+ null
3186
+ )
3187
+ }
3188
+ } catch (e: Exception) {
3189
+ activity.runOnUiThread {
3190
+ webView.evaluateJavascript(
3191
+ "window._craftSharedKeychainReject && window._craftSharedKeychainReject('${e.message}')",
3192
+ null
3193
+ )
3194
+ }
3195
+ }
3196
+ }
3197
+
3198
+ // ==================== Local Auth Persistence ====================
3199
+
3200
+ private var authSessionExpiry: Long = 0L
3201
+
3202
+ @JavascriptInterface
3203
+ fun setAuthPersistence(enabled: Boolean, duration: Long) {
3204
+ try {
3205
+ if (enabled) {
3206
+ authSessionExpiry = System.currentTimeMillis() + (duration * 1000)
3207
+ activity.runOnUiThread {
3208
+ webView.evaluateJavascript(
3209
+ "window._craftAuthPersistResolve && window._craftAuthPersistResolve({enabled: true, duration: $duration, expiresAt: $authSessionExpiry})",
3210
+ null
3211
+ )
3212
+ }
3213
+ } else {
3214
+ authSessionExpiry = 0L
3215
+ activity.runOnUiThread {
3216
+ webView.evaluateJavascript(
3217
+ "window._craftAuthPersistResolve && window._craftAuthPersistResolve({enabled: false})",
3218
+ null
3219
+ )
3220
+ }
3221
+ }
3222
+ } catch (e: Exception) {
3223
+ activity.runOnUiThread {
3224
+ webView.evaluateJavascript(
3225
+ "window._craftAuthPersistReject && window._craftAuthPersistReject('${e.message}')",
3226
+ null
3227
+ )
3228
+ }
3229
+ }
3230
+ }
3231
+
3232
+ @JavascriptInterface
3233
+ fun checkAuthPersistence() {
3234
+ try {
3235
+ val now = System.currentTimeMillis()
3236
+ val isValid = authSessionExpiry > now
3237
+ val remainingMs = if (isValid) authSessionExpiry - now else 0L
3238
+ val remainingSeconds = remainingMs / 1000
3239
+
3240
+ activity.runOnUiThread {
3241
+ webView.evaluateJavascript(
3242
+ "window._craftAuthPersistResolve && window._craftAuthPersistResolve({isValid: $isValid, remainingSeconds: $remainingSeconds})",
3243
+ null
3244
+ )
3245
+ }
3246
+ } catch (e: Exception) {
3247
+ activity.runOnUiThread {
3248
+ webView.evaluateJavascript(
3249
+ "window._craftAuthPersistReject && window._craftAuthPersistReject('${e.message}')",
3250
+ null
3251
+ )
3252
+ }
3253
+ }
3254
+ }
3255
+
3256
+ @JavascriptInterface
3257
+ fun clearAuthPersistence() {
3258
+ try {
3259
+ authSessionExpiry = 0L
3260
+ activity.runOnUiThread {
3261
+ webView.evaluateJavascript(
3262
+ "window._craftAuthPersistResolve && window._craftAuthPersistResolve({cleared: true})",
3263
+ null
3264
+ )
3265
+ }
3266
+ } catch (e: Exception) {
3267
+ activity.runOnUiThread {
3268
+ webView.evaluateJavascript(
3269
+ "window._craftAuthPersistReject && window._craftAuthPersistReject('${e.message}')",
3270
+ null
3271
+ )
3272
+ }
3273
+ }
3274
+ }
3275
+
3276
+ // ==================== AR (ARCore) ====================
3277
+ // Note: ARCore requires additional setup in build.gradle and manifest
3278
+ // This is a placeholder that indicates AR is not available on this platform via simple JS bridge
3279
+ // For full ARCore support, consider using Sceneform or AR Fragment
3280
+
3281
+ @JavascriptInterface
3282
+ fun startAR(optionsJson: String) {
3283
+ activity.runOnUiThread {
3284
+ webView.evaluateJavascript(
3285
+ "window._craftARReject && window._craftARReject('ARCore requires native Activity integration. Use Sceneform or AR Fragment for full AR support.')",
3286
+ null
3287
+ )
3288
+ }
3289
+ }
3290
+
3291
+ @JavascriptInterface
3292
+ fun stopAR() {
3293
+ activity.runOnUiThread {
3294
+ webView.evaluateJavascript(
3295
+ "window._craftARResolve && window._craftARResolve({stopped: true})",
3296
+ null
3297
+ )
3298
+ }
3299
+ }
3300
+
3301
+ @JavascriptInterface
3302
+ fun placeARObject(model: String, positionJson: String) {
3303
+ activity.runOnUiThread {
3304
+ webView.evaluateJavascript(
3305
+ "window._craftARReject && window._craftARReject('ARCore requires native Activity integration')",
3306
+ null
3307
+ )
3308
+ }
3309
+ }
3310
+
3311
+ @JavascriptInterface
3312
+ fun removeARObject(objectId: String) {
3313
+ activity.runOnUiThread {
3314
+ webView.evaluateJavascript(
3315
+ "window._craftARReject && window._craftARReject('ARCore requires native Activity integration')",
3316
+ null
3317
+ )
3318
+ }
3319
+ }
3320
+
3321
+ @JavascriptInterface
3322
+ fun getARPlanes() {
3323
+ activity.runOnUiThread {
3324
+ webView.evaluateJavascript(
3325
+ "window._craftARResolve && window._craftARResolve([])",
3326
+ null
3327
+ )
3328
+ }
3329
+ }
3330
+
3331
+ // ==================== ML Kit ====================
3332
+
3333
+ @JavascriptInterface
3334
+ fun classifyImage(imageBase64: String) {
3335
+ try {
3336
+ val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
3337
+ val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
3338
+ val inputImage = InputImage.fromBitmap(bitmap, 0)
3339
+
3340
+ val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)
3341
+ labeler.process(inputImage)
3342
+ .addOnSuccessListener { labels ->
3343
+ val results = JSONArray()
3344
+ for (label in labels) {
3345
+ val obj = JSONObject()
3346
+ obj.put("label", label.text)
3347
+ obj.put("confidence", label.confidence)
3348
+ obj.put("index", label.index)
3349
+ results.put(obj)
3350
+ }
3351
+ activity.runOnUiThread {
3352
+ webView.evaluateJavascript(
3353
+ "window._craftMLResolve && window._craftMLResolve($results)",
3354
+ null
3355
+ )
3356
+ }
3357
+ }
3358
+ .addOnFailureListener { e ->
3359
+ activity.runOnUiThread {
3360
+ webView.evaluateJavascript(
3361
+ "window._craftMLReject && window._craftMLReject('${e.message}')",
3362
+ null
3363
+ )
3364
+ }
3365
+ }
3366
+ } catch (e: Exception) {
3367
+ activity.runOnUiThread {
3368
+ webView.evaluateJavascript(
3369
+ "window._craftMLReject && window._craftMLReject('${e.message}')",
3370
+ null
3371
+ )
3372
+ }
3373
+ }
3374
+ }
3375
+
3376
+ @JavascriptInterface
3377
+ fun detectObjects(imageBase64: String) {
3378
+ try {
3379
+ val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
3380
+ val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
3381
+ val inputImage = InputImage.fromBitmap(bitmap, 0)
3382
+
3383
+ val options = ObjectDetectorOptions.Builder()
3384
+ .setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
3385
+ .enableMultipleObjects()
3386
+ .enableClassification()
3387
+ .build()
3388
+
3389
+ val objectDetector = ObjectDetection.getClient(options)
3390
+ objectDetector.process(inputImage)
3391
+ .addOnSuccessListener { detectedObjects ->
3392
+ val results = JSONArray()
3393
+ for (obj in detectedObjects) {
3394
+ val objJson = JSONObject()
3395
+
3396
+ // Bounding box
3397
+ val bbox = JSONObject()
3398
+ bbox.put("x", obj.boundingBox.left)
3399
+ bbox.put("y", obj.boundingBox.top)
3400
+ bbox.put("width", obj.boundingBox.width())
3401
+ bbox.put("height", obj.boundingBox.height())
3402
+ objJson.put("boundingBox", bbox)
3403
+
3404
+ // Labels
3405
+ val labels = JSONArray()
3406
+ for (label in obj.labels) {
3407
+ val labelJson = JSONObject()
3408
+ labelJson.put("label", label.text)
3409
+ labelJson.put("confidence", label.confidence)
3410
+ labelJson.put("index", label.index)
3411
+ labels.put(labelJson)
3412
+ }
3413
+ objJson.put("labels", labels)
3414
+ objJson.put("trackingId", obj.trackingId)
3415
+
3416
+ results.put(objJson)
3417
+ }
3418
+ activity.runOnUiThread {
3419
+ webView.evaluateJavascript(
3420
+ "window._craftMLResolve && window._craftMLResolve($results)",
3421
+ null
3422
+ )
3423
+ }
3424
+ }
3425
+ .addOnFailureListener { e ->
3426
+ activity.runOnUiThread {
3427
+ webView.evaluateJavascript(
3428
+ "window._craftMLReject && window._craftMLReject('${e.message}')",
3429
+ null
3430
+ )
3431
+ }
3432
+ }
3433
+ } catch (e: Exception) {
3434
+ activity.runOnUiThread {
3435
+ webView.evaluateJavascript(
3436
+ "window._craftMLReject && window._craftMLReject('${e.message}')",
3437
+ null
3438
+ )
3439
+ }
3440
+ }
3441
+ }
3442
+
3443
+ @JavascriptInterface
3444
+ fun recognizeText(imageBase64: String) {
3445
+ try {
3446
+ val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
3447
+ val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
3448
+ val inputImage = InputImage.fromBitmap(bitmap, 0)
3449
+
3450
+ val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
3451
+ recognizer.process(inputImage)
3452
+ .addOnSuccessListener { visionText ->
3453
+ val results = JSONArray()
3454
+ for (block in visionText.textBlocks) {
3455
+ for (line in block.lines) {
3456
+ val lineJson = JSONObject()
3457
+ lineJson.put("text", line.text)
3458
+ lineJson.put("confidence", line.confidence ?: 0.0)
3459
+
3460
+ val boundingBox = line.boundingBox
3461
+ if (boundingBox != null) {
3462
+ val bbox = JSONObject()
3463
+ bbox.put("x", boundingBox.left)
3464
+ bbox.put("y", boundingBox.top)
3465
+ bbox.put("width", boundingBox.width())
3466
+ bbox.put("height", boundingBox.height())
3467
+ lineJson.put("boundingBox", bbox)
3468
+ }
3469
+
3470
+ results.put(lineJson)
3471
+ }
3472
+ }
3473
+ activity.runOnUiThread {
3474
+ webView.evaluateJavascript(
3475
+ "window._craftMLResolve && window._craftMLResolve($results)",
3476
+ null
3477
+ )
3478
+ }
3479
+ }
3480
+ .addOnFailureListener { e ->
3481
+ activity.runOnUiThread {
3482
+ webView.evaluateJavascript(
3483
+ "window._craftMLReject && window._craftMLReject('${e.message}')",
3484
+ null
3485
+ )
3486
+ }
3487
+ }
3488
+ } catch (e: Exception) {
3489
+ activity.runOnUiThread {
3490
+ webView.evaluateJavascript(
3491
+ "window._craftMLReject && window._craftMLReject('${e.message}')",
3492
+ null
3493
+ )
3494
+ }
3495
+ }
3496
+ }
3497
+
3498
+ // ==================== Widget Support ====================
3499
+
3500
+ private val widgetPrefs by lazy {
3501
+ activity.getSharedPreferences("craft_widget_prefs", Context.MODE_PRIVATE)
3502
+ }
3503
+
3504
+ @JavascriptInterface
3505
+ fun updateWidget(dataJson: String) {
3506
+ try {
3507
+ val data = JSONObject(dataJson)
3508
+ val editor = widgetPrefs.edit()
3509
+
3510
+ if (data.has("title")) {
3511
+ editor.putString("widget_title", data.getString("title"))
3512
+ }
3513
+ if (data.has("subtitle")) {
3514
+ editor.putString("widget_subtitle", data.getString("subtitle"))
3515
+ }
3516
+ if (data.has("value")) {
3517
+ editor.putString("widget_value", data.getString("value"))
3518
+ }
3519
+ if (data.has("icon")) {
3520
+ editor.putString("widget_icon", data.getString("icon"))
3521
+ }
3522
+
3523
+ editor.apply()
3524
+
3525
+ // Notify widgets to update
3526
+ val intent = Intent("{{PACKAGE_NAME}}.WIDGET_UPDATE")
3527
+ intent.setPackage(activity.packageName)
3528
+ activity.sendBroadcast(intent)
3529
+
3530
+ activity.runOnUiThread {
3531
+ webView.evaluateJavascript(
3532
+ "window._craftWidgetResolve && window._craftWidgetResolve({updated: true})",
3533
+ null
3534
+ )
3535
+ }
3536
+ } catch (e: Exception) {
3537
+ activity.runOnUiThread {
3538
+ webView.evaluateJavascript(
3539
+ "window._craftWidgetReject && window._craftWidgetReject('${e.message}')",
3540
+ null
3541
+ )
3542
+ }
3543
+ }
3544
+ }
3545
+
3546
+ @JavascriptInterface
3547
+ fun reloadWidgets() {
3548
+ val intent = Intent("{{PACKAGE_NAME}}.WIDGET_UPDATE")
3549
+ intent.setPackage(activity.packageName)
3550
+ activity.sendBroadcast(intent)
3551
+
3552
+ activity.runOnUiThread {
3553
+ webView.evaluateJavascript(
3554
+ "window._craftWidgetResolve && window._craftWidgetResolve({reloaded: true})",
3555
+ null
3556
+ )
3557
+ }
3558
+ }
3559
+
3560
+ // ==================== Google Assistant / App Actions ====================
3561
+
3562
+ @JavascriptInterface
3563
+ fun registerVoiceAction(phrase: String, action: String) {
3564
+ // App Actions are defined in shortcuts.xml, not dynamically
3565
+ // This stores the action for handling incoming intents
3566
+ val prefs = activity.getSharedPreferences("craft_voice_actions", Context.MODE_PRIVATE)
3567
+ prefs.edit().putString(action, phrase).apply()
3568
+
3569
+ activity.runOnUiThread {
3570
+ webView.evaluateJavascript(
3571
+ "window._craftVoiceResolve && window._craftVoiceResolve({registered: true, action: '$action', phrase: '$phrase'})",
3572
+ null
3573
+ )
3574
+ }
3575
+ }
3576
+
3577
+ @JavascriptInterface
3578
+ fun removeVoiceAction(action: String) {
3579
+ val prefs = activity.getSharedPreferences("craft_voice_actions", Context.MODE_PRIVATE)
3580
+ prefs.edit().remove(action).apply()
3581
+
3582
+ activity.runOnUiThread {
3583
+ webView.evaluateJavascript(
3584
+ "window._craftVoiceResolve && window._craftVoiceResolve({removed: true, action: '$action'})",
3585
+ null
3586
+ )
3587
+ }
3588
+ }
3589
+
3590
+ // Handle incoming voice action from Google Assistant
3591
+ fun handleVoiceAction(intent: Intent?) {
3592
+ intent?.let {
3593
+ val action = it.action
3594
+ val data = it.dataString
3595
+
3596
+ if (action != null) {
3597
+ activity.runOnUiThread {
3598
+ webView.evaluateJavascript(
3599
+ "window.dispatchEvent(new CustomEvent('craftVoiceAction', {detail: {action: '$action', data: '$data'}}));",
3600
+ null
3601
+ )
3602
+ }
3603
+ }
3604
+ }
3605
+ }
3606
+
3607
+ // ==================== Wear OS Connectivity ====================
3608
+
3609
+ @JavascriptInterface
3610
+ fun sendToWatch(messageJson: String) {
3611
+ // Note: Full Wearable API requires separate Wear OS app
3612
+ // This provides placeholder API for future integration
3613
+ activity.runOnUiThread {
3614
+ webView.evaluateJavascript(
3615
+ "window._craftWatchReject && window._craftWatchReject('Wear OS integration requires companion app setup')",
3616
+ null
3617
+ )
3618
+ }
3619
+ }
3620
+
3621
+ @JavascriptInterface
3622
+ fun updateWatchContext(contextJson: String) {
3623
+ // Note: Full Wearable API requires Data Layer API setup
3624
+ activity.runOnUiThread {
3625
+ webView.evaluateJavascript(
3626
+ "window._craftWatchReject && window._craftWatchReject('Wear OS integration requires companion app setup')",
3627
+ null
3628
+ )
3629
+ }
3630
+ }
3631
+
3632
+ @JavascriptInterface
3633
+ fun isWatchReachable(): Boolean {
3634
+ // Note: Would use CapabilityClient to check for paired watch
3635
+ return false
3636
+ }
3637
+
3638
+ // ==================== Deep Links ====================
3639
+
3640
+ private var initialURL: String? = null
3641
+
3642
+ fun setInitialURL(url: String?) {
3643
+ initialURL = url
3644
+ }
3645
+
3646
+ @JavascriptInterface
3647
+ fun getInitialURL() {
3648
+ activity.runOnUiThread {
3649
+ if (initialURL != null) {
3650
+ val uri = Uri.parse(initialURL)
3651
+ val json = JSONObject().apply {
3652
+ put("url", initialURL)
3653
+ put("scheme", uri.scheme ?: "")
3654
+ put("host", uri.host ?: "")
3655
+ put("path", uri.path ?: "")
3656
+ put("query", uri.query ?: "")
3657
+ // Parse query parameters
3658
+ val queryParams = JSONObject()
3659
+ uri.queryParameterNames.forEach { name ->
3660
+ queryParams.put(name, uri.getQueryParameter(name) ?: "")
3661
+ }
3662
+ put("queryParams", queryParams)
3663
+ }
3664
+ webView.evaluateJavascript(
3665
+ "window._craftDeepLinkResolve && window._craftDeepLinkResolve($json)",
3666
+ null
3667
+ )
3668
+ } else {
3669
+ webView.evaluateJavascript(
3670
+ "window._craftDeepLinkResolve && window._craftDeepLinkResolve(null)",
3671
+ null
3672
+ )
3673
+ }
3674
+ }
3675
+ }
3676
+
3677
+ fun dispatchDeepLink(url: String) {
3678
+ // Store as initial URL if this is the first one
3679
+ if (initialURL == null) {
3680
+ initialURL = url
3681
+ }
3682
+
3683
+ val uri = Uri.parse(url)
3684
+ val json = JSONObject().apply {
3685
+ put("url", url)
3686
+ put("scheme", uri.scheme ?: "")
3687
+ put("host", uri.host ?: "")
3688
+ put("path", uri.path ?: "")
3689
+ put("query", uri.query ?: "")
3690
+ // Parse query parameters
3691
+ val queryParams = JSONObject()
3692
+ uri.queryParameterNames.forEach { name ->
3693
+ queryParams.put(name, uri.getQueryParameter(name) ?: "")
3694
+ }
3695
+ put("queryParams", queryParams)
3696
+ }
3697
+
3698
+ activity.runOnUiThread {
3699
+ webView.evaluateJavascript(
3700
+ "window.dispatchEvent(new CustomEvent('craftDeepLink', {detail: $json}));",
3701
+ null
3702
+ )
3703
+ }
3704
+ }
3705
+
3706
+ // ==================== Performance Profiling ====================
3707
+
3708
+ @JavascriptInterface
3709
+ fun getMemoryUsage(): String {
3710
+ val runtime = Runtime.getRuntime()
3711
+ val usedMemory = runtime.totalMemory() - runtime.freeMemory()
3712
+ val maxMemory = runtime.maxMemory()
3713
+ val totalMemory = runtime.totalMemory()
3714
+
3715
+ return JSONObject().apply {
3716
+ put("usedMB", Math.round(usedMemory / 1024.0 / 1024.0 * 100) / 100.0)
3717
+ put("maxMB", Math.round(maxMemory / 1024.0 / 1024.0 * 100) / 100.0)
3718
+ put("totalMB", Math.round(totalMemory / 1024.0 / 1024.0 * 100) / 100.0)
3719
+ put("usedBytes", usedMemory)
3720
+ put("maxBytes", maxMemory)
3721
+ }.toString()
3722
+ }
3723
+
3724
+ // ==================== Helpers ====================
3725
+
3726
+ private fun sendEvent(event: String, data: Map<String, Any>) {
3727
+ val json = JSONObject(data).toString()
3728
+ activity.runOnUiThread {
3729
+ webView.evaluateJavascript(
3730
+ "window.dispatchEvent(new CustomEvent('$event', {detail: $json}));",
3731
+ null
3732
+ )
3733
+ }
3734
+ }
3735
+
3736
+ companion object {
3737
+ const val REQUEST_CAMERA = 1001
3738
+ const val REQUEST_GALLERY = 1002
3739
+ const val REQUEST_LOCATION = 1003
3740
+ const val REQUEST_CONTACTS = 1004
3741
+ const val REQUEST_CALENDAR = 1005
3742
+ const val REQUEST_FILE_PICKER = 1006
3743
+ const val REQUEST_AUDIO = 1007
3744
+ const val REQUEST_VIDEO = 1008
3745
+ const val REQUEST_BLUETOOTH = 1009
3746
+ const val REQUEST_PICK_CONTACT = 1010
3747
+ }
3748
+ }