craft-native 0.0.89 → 0.0.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/src/index.js +285 -44
- package/dist/android/src/promise-runtime.d.ts +3 -0
- package/dist/android/templates/CraftBridge.kt.template +1951 -1193
- package/dist/android/templates/CraftHealthConnect.kt.template +45 -6
- package/dist/android/templates/CraftHealthConnectStub.kt.template +7 -1
- package/dist/android/templates/CraftNative.kt.template +2231 -0
- package/dist/android/templates/LocationRecordingService.kt.template +34 -9
- package/dist/android/templates/MainActivity.kt.template +25 -2
- package/dist/android/templates/proguard-rules.pro.template +4 -1
- package/dist/android/templates/test-bridges.html +10 -33
- package/dist/api/index.d.ts +1 -1
- package/dist/api/ios-advanced.d.ts +8 -5
- package/dist/api/live-activity-handle.d.ts +6 -0
- package/dist/api/mobile.d.ts +13 -5
- package/dist/api/window.d.ts +2 -0
- package/dist/cli.js +404 -128
- package/dist/index.cjs +65 -17
- package/dist/index.js +65 -17
- package/dist/ios/src/index.js +22 -4
- package/dist/ios/templates/CraftApp.swift +473 -60
- package/dist/ios/templates/CraftWatchApp.swift.template +8 -0
- package/dist/ios/templates/WatchApp.Info.plist.template +2 -0
- package/dist/ios/templates/WatchExtension.Info.plist.template +34 -0
- package/dist/ios/templates/project.yml.template +10 -4
- package/dist/mobile.js +36 -13
- package/dist/scaffold-version.d.ts +5 -0
- package/package.json +1 -1
- package/dist/android/templates/CraftBridgeExtensions.kt.template +0 -383
- package/dist/android/templates/CraftWidgetProvider.kt.template +0 -246
|
@@ -18,12 +18,17 @@ import android.net.NetworkCapabilities
|
|
|
18
18
|
import android.net.NetworkRequest
|
|
19
19
|
import android.net.Uri
|
|
20
20
|
import android.os.Build
|
|
21
|
+
import com.craft.runtime.CraftNative
|
|
22
|
+
import com.craft.runtime.CraftLocationRecordingStore
|
|
23
|
+
import com.craft.runtime.LocationRecordingService
|
|
21
24
|
import android.os.Bundle
|
|
25
|
+
import android.os.Handler
|
|
22
26
|
import android.os.Looper
|
|
23
27
|
import android.os.VibrationEffect
|
|
24
28
|
import android.os.Vibrator
|
|
25
29
|
import android.os.VibratorManager
|
|
26
30
|
import android.provider.MediaStore
|
|
31
|
+
import android.provider.OpenableColumns
|
|
27
32
|
import android.provider.Settings
|
|
28
33
|
import android.speech.RecognitionListener
|
|
29
34
|
import android.speech.RecognizerIntent
|
|
@@ -34,7 +39,6 @@ import android.webkit.WebView
|
|
|
34
39
|
import androidx.biometric.BiometricManager
|
|
35
40
|
import androidx.biometric.BiometricPrompt
|
|
36
41
|
import androidx.core.app.ActivityCompat
|
|
37
|
-
import androidx.core.app.NotificationManagerCompat
|
|
38
42
|
import androidx.core.content.ContextCompat
|
|
39
43
|
import androidx.fragment.app.FragmentActivity
|
|
40
44
|
import androidx.lifecycle.Lifecycle
|
|
@@ -48,6 +52,8 @@ import com.google.android.gms.location.LocationRequest
|
|
|
48
52
|
import com.google.android.gms.location.LocationResult
|
|
49
53
|
import com.google.android.gms.location.LocationServices
|
|
50
54
|
import com.google.android.gms.location.Priority
|
|
55
|
+
import com.google.android.gms.common.ConnectionResult
|
|
56
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
51
57
|
import com.google.android.play.core.review.ReviewManagerFactory
|
|
52
58
|
{{FIREBASE_IMPORT}}
|
|
53
59
|
import android.content.ContentResolver
|
|
@@ -98,19 +104,130 @@ import java.io.File
|
|
|
98
104
|
import java.util.concurrent.TimeUnit
|
|
99
105
|
import java.util.UUID
|
|
100
106
|
|
|
107
|
+
internal object CraftPermissionPolicy {
|
|
108
|
+
data class Request(
|
|
109
|
+
val groupIndex: Int,
|
|
110
|
+
val permissions: Array<String>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
private fun permissionGroups(name: String, sdkInt: Int): List<List<String>>? = when (name) {
|
|
114
|
+
"camera" -> listOf(listOf(Manifest.permission.CAMERA))
|
|
115
|
+
"microphone" -> listOf(listOf(Manifest.permission.RECORD_AUDIO))
|
|
116
|
+
"photos" -> listOf(listOf(
|
|
117
|
+
if (sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
|
118
|
+
Manifest.permission.READ_MEDIA_IMAGES
|
|
119
|
+
} else {
|
|
120
|
+
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
121
|
+
}
|
|
122
|
+
))
|
|
123
|
+
// Android presents precise and approximate location as one choice. A
|
|
124
|
+
// coarse grant is usable foreground location access even when the
|
|
125
|
+
// corresponding fine result is denied.
|
|
126
|
+
"location" -> listOf(listOf(
|
|
127
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
128
|
+
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
129
|
+
))
|
|
130
|
+
"locationAlways" -> if (sdkInt >= Build.VERSION_CODES.Q) {
|
|
131
|
+
listOf(
|
|
132
|
+
listOf(
|
|
133
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
134
|
+
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
135
|
+
),
|
|
136
|
+
listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
137
|
+
)
|
|
138
|
+
} else {
|
|
139
|
+
listOf(listOf(
|
|
140
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
141
|
+
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
142
|
+
))
|
|
143
|
+
}
|
|
144
|
+
"notifications" -> if (sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
|
145
|
+
listOf(listOf(Manifest.permission.POST_NOTIFICATIONS))
|
|
146
|
+
} else {
|
|
147
|
+
emptyList()
|
|
148
|
+
}
|
|
149
|
+
"contacts" -> listOf(listOf(Manifest.permission.READ_CONTACTS))
|
|
150
|
+
"calendar" -> listOf(listOf(Manifest.permission.READ_CALENDAR))
|
|
151
|
+
"bluetooth" -> if (sdkInt >= Build.VERSION_CODES.S) {
|
|
152
|
+
listOf(listOf(Manifest.permission.BLUETOOTH_SCAN))
|
|
153
|
+
} else {
|
|
154
|
+
emptyList()
|
|
155
|
+
}
|
|
156
|
+
"motion" -> if (sdkInt >= Build.VERSION_CODES.Q) {
|
|
157
|
+
listOf(listOf(Manifest.permission.ACTIVITY_RECOGNITION))
|
|
158
|
+
} else {
|
|
159
|
+
emptyList()
|
|
160
|
+
}
|
|
161
|
+
else -> null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fun requiredPermissions(name: String, sdkInt: Int): Array<String>? =
|
|
165
|
+
permissionGroups(name, sdkInt)?.flatten()?.distinct()?.toTypedArray()
|
|
166
|
+
|
|
167
|
+
fun status(name: String, sdkInt: Int, isGranted: (String) -> Boolean): String {
|
|
168
|
+
val groups = permissionGroups(name, sdkInt) ?: return "undetermined"
|
|
169
|
+
return if (groups.all { group -> group.any(isGranted) }) "granted" else "denied"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fun nextRequest(
|
|
173
|
+
name: String,
|
|
174
|
+
sdkInt: Int,
|
|
175
|
+
startingAt: Int,
|
|
176
|
+
isGranted: (String) -> Boolean
|
|
177
|
+
): Request? {
|
|
178
|
+
val groups = permissionGroups(name, sdkInt) ?: return null
|
|
179
|
+
val groupIndex = (startingAt until groups.size).firstOrNull { index ->
|
|
180
|
+
groups[index].none(isGranted)
|
|
181
|
+
} ?: return null
|
|
182
|
+
return Request(groupIndex, groups[groupIndex].toTypedArray())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fun groupIsGranted(
|
|
186
|
+
name: String,
|
|
187
|
+
sdkInt: Int,
|
|
188
|
+
groupIndex: Int,
|
|
189
|
+
isGranted: (String) -> Boolean
|
|
190
|
+
): Boolean = permissionGroups(name, sdkInt)
|
|
191
|
+
?.getOrNull(groupIndex)
|
|
192
|
+
?.any(isGranted) == true
|
|
193
|
+
|
|
194
|
+
fun foregroundLocationIsGranted(isGranted: (String) -> Boolean): Boolean =
|
|
195
|
+
isGranted(Manifest.permission.ACCESS_FINE_LOCATION) ||
|
|
196
|
+
isGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private data class PendingPermissionRequest(
|
|
200
|
+
val callbackId: Int,
|
|
201
|
+
val permission: String,
|
|
202
|
+
val groupIndex: Int
|
|
203
|
+
)
|
|
204
|
+
|
|
101
205
|
class CraftBridge(
|
|
102
206
|
private val activity: Activity,
|
|
103
207
|
private val webView: WebView,
|
|
104
208
|
private val healthConnect: CraftHealthConnect
|
|
105
209
|
) {
|
|
210
|
+
@Volatile
|
|
211
|
+
private var closed = false
|
|
212
|
+
private var nativeDeepLinks = CraftNative.resetDeepLinks()
|
|
106
213
|
private var speechRecognizer: SpeechRecognizer? = null
|
|
214
|
+
private var biometricPrompt: BiometricPrompt? = null
|
|
107
215
|
private var isListening = false
|
|
108
216
|
private val mainExecutor: Executor = ContextCompat.getMainExecutor(activity)
|
|
217
|
+
@Volatile
|
|
218
|
+
private var bridgeReady = false
|
|
219
|
+
private val pendingEvents = mutableListOf<Pair<String, Map<String, Any>>>()
|
|
109
220
|
|
|
110
221
|
// Location tracking
|
|
111
222
|
private var fusedLocationClient: FusedLocationProviderClient? = null
|
|
112
|
-
private
|
|
223
|
+
private val locationCallbacks = mutableMapOf<Int, LocationCallback>()
|
|
224
|
+
private val oneShotLocationCallbacks = mutableSetOf<LocationCallback>()
|
|
225
|
+
private val oneShotLocationHandler = Handler(Looper.getMainLooper())
|
|
226
|
+
private val oneShotLocationTimeouts = mutableMapOf<LocationCallback, Runnable>()
|
|
227
|
+
private val currentPositionSequence = java.util.concurrent.atomic.AtomicLong(0)
|
|
113
228
|
private var watchId = 0
|
|
229
|
+
private val pendingPermissionRequests = mutableMapOf<Int, PendingPermissionRequest>()
|
|
230
|
+
private var nextPermissionRequestCode = PERMISSION_REQUEST_START
|
|
114
231
|
|
|
115
232
|
// Network monitoring
|
|
116
233
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
|
@@ -124,8 +241,9 @@ class CraftBridge(
|
|
|
124
241
|
// Keep awake
|
|
125
242
|
private var isKeepingAwake = false
|
|
126
243
|
|
|
127
|
-
// Billing
|
|
128
|
-
private var
|
|
244
|
+
// Billing clients for in-app purchases
|
|
245
|
+
private var productBillingClient: BillingClient? = null
|
|
246
|
+
private var restoreBillingClient: BillingClient? = null
|
|
129
247
|
|
|
130
248
|
// Notification channel ID
|
|
131
249
|
private val notificationChannelId = "craft_notifications"
|
|
@@ -165,8 +283,33 @@ class CraftBridge(
|
|
|
165
283
|
)
|
|
166
284
|
}
|
|
167
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Quote a value for interpolation into JavaScript.
|
|
288
|
+
*
|
|
289
|
+
* The result carries its own quotes, so write `f($payload)` — leaving the
|
|
290
|
+
* surrounding `'...'` in place would quote it twice.
|
|
291
|
+
*
|
|
292
|
+
* Without this, a value containing an apostrophe ends the JS string early, the
|
|
293
|
+
* script fails to parse, and `evaluateJavascript` runs nothing at all — so a
|
|
294
|
+
* hand-built promise with no timeout never settles.
|
|
295
|
+
*
|
|
296
|
+
* `Any?` rather than `String?`, because these call sites replaced string
|
|
297
|
+
* interpolation and interpolation accepts anything: `contactId` is the `Long`
|
|
298
|
+
* from `ContentUris.parseId`, `downloadId` the `Long` from
|
|
299
|
+
* `DownloadManager.enqueue`, and `errString` the `CharSequence` a
|
|
300
|
+
* `BiometricPrompt` error arrives as. `Any?.toString()` is exactly what `$x`
|
|
301
|
+
* did, null included — it renders as the string "null", the way it always has.
|
|
302
|
+
*/
|
|
303
|
+
private fun jsQuote(value: Any?): String = JSONObject.quote(value.toString())
|
|
304
|
+
|
|
168
305
|
fun injectBridge() {
|
|
306
|
+
// Before the script, so a native callback arriving while the page is
|
|
307
|
+
// still loading has somewhere to go rather than being counted as
|
|
308
|
+
// dropped.
|
|
309
|
+
installNativeDeliverer()
|
|
310
|
+
|
|
169
311
|
val script = """
|
|
312
|
+
{{PROMISE_RUNTIME}}
|
|
170
313
|
window.craft = {
|
|
171
314
|
platform: 'android',
|
|
172
315
|
capabilities: {
|
|
@@ -181,7 +324,7 @@ class CraftBridge(
|
|
|
181
324
|
backgroundLocation: {{ENABLE_BACKGROUND_LOCATION}},
|
|
182
325
|
clipboard: false,
|
|
183
326
|
deviceInfo: true,
|
|
184
|
-
appBadge:
|
|
327
|
+
appBadge: false,
|
|
185
328
|
networkStatus: true,
|
|
186
329
|
appReview: true,
|
|
187
330
|
flashlight: {{ENABLE_CAMERA}} && ${hasFlashlight()},
|
|
@@ -229,34 +372,26 @@ class CraftBridge(
|
|
|
229
372
|
|
|
230
373
|
// Camera
|
|
231
374
|
openCamera: function() {
|
|
232
|
-
return
|
|
233
|
-
window._craftCameraResolve = resolve;
|
|
234
|
-
window._craftCameraReject = reject;
|
|
375
|
+
return window.__craftPromise('camera', '_craftCameraResolve', '_craftCameraReject', function() {
|
|
235
376
|
CraftAndroid.openCamera();
|
|
236
377
|
});
|
|
237
378
|
},
|
|
238
379
|
pickImage: function() {
|
|
239
|
-
return
|
|
240
|
-
window._craftGalleryResolve = resolve;
|
|
241
|
-
window._craftGalleryReject = reject;
|
|
380
|
+
return window.__craftPromise('gallery', '_craftGalleryResolve', '_craftGalleryReject', function() {
|
|
242
381
|
CraftAndroid.pickImage();
|
|
243
382
|
});
|
|
244
383
|
},
|
|
245
384
|
|
|
246
385
|
// Biometric auth
|
|
247
386
|
authenticate: function(reason) {
|
|
248
|
-
return
|
|
249
|
-
window._craftBiometricResolve = resolve;
|
|
250
|
-
window._craftBiometricReject = reject;
|
|
387
|
+
return window.__craftPromise('biometric', '_craftBiometricResolve', '_craftBiometricReject', function() {
|
|
251
388
|
CraftAndroid.authenticate(reason || 'Authenticate to continue');
|
|
252
389
|
});
|
|
253
390
|
},
|
|
254
391
|
|
|
255
392
|
// Push notifications
|
|
256
393
|
registerPush: function() {
|
|
257
|
-
return
|
|
258
|
-
window._craftPushResolve = resolve;
|
|
259
|
-
window._craftPushReject = reject;
|
|
394
|
+
return window.__craftPromise('push registration', '_craftPushResolve', '_craftPushReject', function() {
|
|
260
395
|
CraftAndroid.registerPush();
|
|
261
396
|
});
|
|
262
397
|
},
|
|
@@ -276,11 +411,14 @@ class CraftBridge(
|
|
|
276
411
|
|
|
277
412
|
// Geolocation
|
|
278
413
|
getCurrentPosition: function(options) {
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
414
|
+
return window.__craftPromise(
|
|
415
|
+
'location',
|
|
416
|
+
'_craftLocationResolve',
|
|
417
|
+
'_craftLocationReject',
|
|
418
|
+
function() { CraftAndroid.getCurrentPosition(JSON.stringify(options || {})); },
|
|
419
|
+
15000,
|
|
420
|
+
{code: 2, message: 'Location request timed out; Google Play Services or a location provider may be unavailable'}
|
|
421
|
+
);
|
|
284
422
|
},
|
|
285
423
|
watchPosition: function(callback, options) {
|
|
286
424
|
var watchId = CraftAndroid.watchPosition(JSON.stringify(options || {}));
|
|
@@ -312,11 +450,11 @@ class CraftBridge(
|
|
|
312
450
|
},
|
|
313
451
|
|
|
314
452
|
// App Badge
|
|
315
|
-
setBadge: function(
|
|
316
|
-
|
|
453
|
+
setBadge: function() {
|
|
454
|
+
throw new Error('App badges are unavailable on Android');
|
|
317
455
|
},
|
|
318
456
|
clearBadge: function() {
|
|
319
|
-
|
|
457
|
+
throw new Error('App badges are unavailable on Android');
|
|
320
458
|
},
|
|
321
459
|
|
|
322
460
|
// Network Status
|
|
@@ -334,9 +472,7 @@ class CraftBridge(
|
|
|
334
472
|
|
|
335
473
|
// App Review
|
|
336
474
|
requestReview: function() {
|
|
337
|
-
return
|
|
338
|
-
window._craftReviewResolve = resolve;
|
|
339
|
-
window._craftReviewReject = reject;
|
|
475
|
+
return window.__craftPromise('app review', '_craftReviewResolve', '_craftReviewReject', function() {
|
|
340
476
|
CraftAndroid.requestReview();
|
|
341
477
|
});
|
|
342
478
|
},
|
|
@@ -375,16 +511,12 @@ class CraftBridge(
|
|
|
375
511
|
// Contacts
|
|
376
512
|
contacts: {
|
|
377
513
|
getAll: function() {
|
|
378
|
-
return
|
|
379
|
-
window._craftContactsResolve = resolve;
|
|
380
|
-
window._craftContactsReject = reject;
|
|
514
|
+
return window.__craftPromise('contacts read', '_craftContactsResolve', '_craftContactsReject', function() {
|
|
381
515
|
CraftAndroid.getContacts();
|
|
382
516
|
});
|
|
383
517
|
},
|
|
384
518
|
add: function(contact) {
|
|
385
|
-
return
|
|
386
|
-
window._craftAddContactResolve = resolve;
|
|
387
|
-
window._craftAddContactReject = reject;
|
|
519
|
+
return window.__craftPromise('contact write', '_craftAddContactResolve', '_craftAddContactReject', function() {
|
|
388
520
|
CraftAndroid.addContact(JSON.stringify(contact));
|
|
389
521
|
});
|
|
390
522
|
}
|
|
@@ -393,23 +525,17 @@ class CraftBridge(
|
|
|
393
525
|
// Calendar
|
|
394
526
|
calendar: {
|
|
395
527
|
getEvents: function(startDate, endDate) {
|
|
396
|
-
return
|
|
397
|
-
window._craftCalendarResolve = resolve;
|
|
398
|
-
window._craftCalendarReject = reject;
|
|
528
|
+
return window.__craftPromise('calendar read', '_craftCalendarResolve', '_craftCalendarReject', function() {
|
|
399
529
|
CraftAndroid.getCalendarEvents(startDate || 0, endDate || 0);
|
|
400
530
|
});
|
|
401
531
|
},
|
|
402
532
|
createEvent: function(event) {
|
|
403
|
-
return
|
|
404
|
-
window._craftCreateEventResolve = resolve;
|
|
405
|
-
window._craftCreateEventReject = reject;
|
|
533
|
+
return window.__craftPromise('calendar create', '_craftCreateEventResolve', '_craftCreateEventReject', function() {
|
|
406
534
|
CraftAndroid.createCalendarEvent(JSON.stringify(event));
|
|
407
535
|
});
|
|
408
536
|
},
|
|
409
537
|
deleteEvent: function(eventId) {
|
|
410
|
-
return
|
|
411
|
-
window._craftDeleteEventResolve = resolve;
|
|
412
|
-
window._craftDeleteEventReject = reject;
|
|
538
|
+
return window.__craftPromise('calendar delete', '_craftDeleteEventResolve', '_craftDeleteEventReject', function() {
|
|
413
539
|
CraftAndroid.deleteCalendarEvent(eventId);
|
|
414
540
|
});
|
|
415
541
|
}
|
|
@@ -418,9 +544,7 @@ class CraftBridge(
|
|
|
418
544
|
// Local Notifications
|
|
419
545
|
notifications: {
|
|
420
546
|
schedule: function(notification) {
|
|
421
|
-
return
|
|
422
|
-
window._craftNotifResolve = resolve;
|
|
423
|
-
window._craftNotifReject = reject;
|
|
547
|
+
return window.__craftPromise('notification schedule', '_craftNotifResolve', '_craftNotifReject', function() {
|
|
424
548
|
CraftAndroid.scheduleNotification(JSON.stringify(notification));
|
|
425
549
|
});
|
|
426
550
|
},
|
|
@@ -435,23 +559,17 @@ class CraftBridge(
|
|
|
435
559
|
// In-App Purchase
|
|
436
560
|
iap: {
|
|
437
561
|
getProducts: function(productIds) {
|
|
438
|
-
return
|
|
439
|
-
window._craftProductsResolve = resolve;
|
|
440
|
-
window._craftProductsReject = reject;
|
|
562
|
+
return window.__craftPromise('products', '_craftProductsResolve', '_craftProductsReject', function() {
|
|
441
563
|
CraftAndroid.getProducts(JSON.stringify(productIds));
|
|
442
564
|
});
|
|
443
565
|
},
|
|
444
566
|
purchase: function(productId) {
|
|
445
|
-
return
|
|
446
|
-
window._craftPurchaseResolve = resolve;
|
|
447
|
-
window._craftPurchaseReject = reject;
|
|
567
|
+
return window.__craftPromise('purchase', '_craftPurchaseResolve', '_craftPurchaseReject', function() {
|
|
448
568
|
CraftAndroid.purchase(productId);
|
|
449
569
|
});
|
|
450
570
|
},
|
|
451
571
|
restore: function() {
|
|
452
|
-
return
|
|
453
|
-
window._craftRestoreResolve = resolve;
|
|
454
|
-
window._craftRestoreReject = reject;
|
|
572
|
+
return window.__craftPromise('purchase restore', '_craftRestoreResolve', '_craftRestoreReject', function() {
|
|
455
573
|
CraftAndroid.restorePurchases();
|
|
456
574
|
});
|
|
457
575
|
}
|
|
@@ -477,68 +595,52 @@ class CraftBridge(
|
|
|
477
595
|
|
|
478
596
|
// QR/Barcode Scanner
|
|
479
597
|
scanQRCode: function() {
|
|
480
|
-
return
|
|
481
|
-
window._craftQRResolve = resolve;
|
|
482
|
-
window._craftQRReject = reject;
|
|
598
|
+
return window.__craftPromise('QR scan', '_craftQRResolve', '_craftQRReject', function() {
|
|
483
599
|
CraftAndroid.scanQRCode();
|
|
484
600
|
});
|
|
485
601
|
},
|
|
486
602
|
|
|
487
603
|
// File Picker
|
|
488
604
|
pickFile: function(types) {
|
|
489
|
-
return
|
|
490
|
-
window._craftFileResolve = resolve;
|
|
491
|
-
window._craftFileReject = reject;
|
|
605
|
+
return window.__craftPromise('file picker', '_craftFileResolve', '_craftFileReject', function() {
|
|
492
606
|
CraftAndroid.pickFile(JSON.stringify(types || []));
|
|
493
607
|
});
|
|
494
608
|
},
|
|
495
609
|
|
|
496
610
|
// File Download
|
|
497
611
|
downloadFile: function(url, filename) {
|
|
498
|
-
return
|
|
499
|
-
window._craftDownloadResolve = resolve;
|
|
500
|
-
window._craftDownloadReject = reject;
|
|
612
|
+
return window.__craftPromise('file download', '_craftDownloadResolve', '_craftDownloadReject', function() {
|
|
501
613
|
CraftAndroid.downloadFile(url, filename);
|
|
502
614
|
});
|
|
503
615
|
},
|
|
504
616
|
saveFile: function(data, filename) {
|
|
505
|
-
return
|
|
506
|
-
window._craftSaveResolve = resolve;
|
|
507
|
-
window._craftSaveReject = reject;
|
|
617
|
+
return window.__craftPromise('file save', '_craftSaveResolve', '_craftSaveReject', function() {
|
|
508
618
|
CraftAndroid.saveFile(data, filename);
|
|
509
619
|
});
|
|
510
620
|
},
|
|
511
621
|
|
|
512
622
|
// Google Sign In
|
|
513
623
|
signInWithGoogle: function() {
|
|
514
|
-
return
|
|
515
|
-
window._craftGoogleResolve = resolve;
|
|
516
|
-
window._craftGoogleReject = reject;
|
|
624
|
+
return window.__craftPromise('Google sign in', '_craftGoogleResolve', '_craftGoogleReject', function() {
|
|
517
625
|
CraftAndroid.signInWithGoogle();
|
|
518
626
|
});
|
|
519
627
|
},
|
|
520
628
|
|
|
521
629
|
// Audio Recording
|
|
522
630
|
startAudioRecording: function() {
|
|
523
|
-
return
|
|
524
|
-
window._craftAudioResolve = resolve;
|
|
525
|
-
window._craftAudioReject = reject;
|
|
631
|
+
return window.__craftPromise('audio start', '_craftAudioResolve', '_craftAudioReject', function() {
|
|
526
632
|
CraftAndroid.startAudioRecording();
|
|
527
633
|
});
|
|
528
634
|
},
|
|
529
635
|
stopAudioRecording: function() {
|
|
530
|
-
return
|
|
531
|
-
window._craftAudioStopResolve = resolve;
|
|
532
|
-
window._craftAudioStopReject = reject;
|
|
636
|
+
return window.__craftPromise('audio stop', '_craftAudioStopResolve', '_craftAudioStopReject', function() {
|
|
533
637
|
CraftAndroid.stopAudioRecording();
|
|
534
638
|
});
|
|
535
639
|
},
|
|
536
640
|
|
|
537
641
|
// Video Recording
|
|
538
642
|
startVideoRecording: function() {
|
|
539
|
-
return
|
|
540
|
-
window._craftVideoResolve = resolve;
|
|
541
|
-
window._craftVideoReject = reject;
|
|
643
|
+
return window.__craftPromise('video recording', '_craftVideoResolve', '_craftVideoReject', function() {
|
|
542
644
|
CraftAndroid.startVideoRecording();
|
|
543
645
|
});
|
|
544
646
|
},
|
|
@@ -557,16 +659,12 @@ class CraftBridge(
|
|
|
557
659
|
// Local Database
|
|
558
660
|
db: {
|
|
559
661
|
execute: function(sql, params) {
|
|
560
|
-
return
|
|
561
|
-
window._craftDbExecResolve = resolve;
|
|
562
|
-
window._craftDbExecReject = reject;
|
|
662
|
+
return window.__craftPromise('database execute', '_craftDbExecResolve', '_craftDbExecReject', function() {
|
|
563
663
|
CraftAndroid.dbExecute(sql, JSON.stringify(params || []));
|
|
564
664
|
});
|
|
565
665
|
},
|
|
566
666
|
query: function(sql, params) {
|
|
567
|
-
return
|
|
568
|
-
window._craftDbQueryResolve = resolve;
|
|
569
|
-
window._craftDbQueryReject = reject;
|
|
667
|
+
return window.__craftPromise('database query', '_craftDbQueryResolve', '_craftDbQueryReject', function() {
|
|
570
668
|
CraftAndroid.dbQuery(sql, JSON.stringify(params || []));
|
|
571
669
|
});
|
|
572
670
|
}
|
|
@@ -575,9 +673,7 @@ class CraftBridge(
|
|
|
575
673
|
// Bluetooth
|
|
576
674
|
bluetooth: {
|
|
577
675
|
startScan: function() {
|
|
578
|
-
return
|
|
579
|
-
window._craftBleResolve = resolve;
|
|
580
|
-
window._craftBleReject = reject;
|
|
676
|
+
return window.__craftPromise('Bluetooth scan', '_craftBleResolve', '_craftBleReject', function() {
|
|
581
677
|
CraftAndroid.startBluetoothScan();
|
|
582
678
|
});
|
|
583
679
|
},
|
|
@@ -591,9 +687,7 @@ class CraftBridge(
|
|
|
591
687
|
|
|
592
688
|
// NFC
|
|
593
689
|
scanNFC: function() {
|
|
594
|
-
return
|
|
595
|
-
window._craftNfcResolve = resolve;
|
|
596
|
-
window._craftNfcReject = reject;
|
|
690
|
+
return window.__craftPromise('NFC scan', '_craftNfcResolve', '_craftNfcReject', function() {
|
|
597
691
|
CraftAndroid.scanNFC();
|
|
598
692
|
});
|
|
599
693
|
},
|
|
@@ -601,34 +695,91 @@ class CraftBridge(
|
|
|
601
695
|
// Fitness / Health
|
|
602
696
|
health: {
|
|
603
697
|
requestAuthorization: function(types) {
|
|
604
|
-
return
|
|
605
|
-
window._craftFitnessAuthResolve = resolve;
|
|
606
|
-
window._craftFitnessAuthReject = reject;
|
|
698
|
+
return window.__craftPromise('health authorization', '_craftFitnessAuthResolve', '_craftFitnessAuthReject', function() {
|
|
607
699
|
CraftAndroid.requestFitnessAuthorization(JSON.stringify(types || []));
|
|
608
700
|
});
|
|
609
701
|
},
|
|
610
702
|
getData: function(type, options) {
|
|
611
703
|
options = options || {};
|
|
612
|
-
return
|
|
613
|
-
window._craftFitnessDataResolve = resolve;
|
|
614
|
-
window._craftFitnessDataReject = reject;
|
|
704
|
+
return window.__craftPromise('health read', '_craftFitnessDataResolve', '_craftFitnessDataReject', function() {
|
|
615
705
|
CraftAndroid.getFitnessData(type, options.startDate || 0, options.endDate || 0);
|
|
616
706
|
});
|
|
617
707
|
},
|
|
618
708
|
saveWorkout: function(workout) {
|
|
619
|
-
return
|
|
620
|
-
window._craftFitnessSaveResolve = resolve;
|
|
621
|
-
window._craftFitnessSaveReject = reject;
|
|
709
|
+
return window.__craftPromise('health write', '_craftFitnessSaveResolve', '_craftFitnessSaveReject', function() {
|
|
622
710
|
CraftAndroid.saveHealthWorkout(JSON.stringify(workout || {}));
|
|
623
711
|
});
|
|
624
712
|
}
|
|
625
713
|
},
|
|
626
714
|
|
|
715
|
+
// Flat SDK compatibility methods. Delegate to the namespaced
|
|
716
|
+
// bridge where Android implements the capability, and reject
|
|
717
|
+
// explicitly for APIs that are intentionally iOS-only.
|
|
718
|
+
getContacts: function() {
|
|
719
|
+
return window.craft.contacts.getAll();
|
|
720
|
+
},
|
|
721
|
+
addContact: function(contact) {
|
|
722
|
+
return window.craft.contacts.add(contact);
|
|
723
|
+
},
|
|
724
|
+
getCalendarEvents: function(startDate, endDate) {
|
|
725
|
+
return window.craft.calendar.getEvents(startDate, endDate);
|
|
726
|
+
},
|
|
727
|
+
createCalendarEvent: function(event) {
|
|
728
|
+
return window.craft.calendar.createEvent(event);
|
|
729
|
+
},
|
|
730
|
+
deleteCalendarEvent: function(eventId) {
|
|
731
|
+
return window.craft.calendar.deleteEvent(eventId);
|
|
732
|
+
},
|
|
733
|
+
scheduleNotification: function(notification) {
|
|
734
|
+
return window.craft.notifications.schedule(notification);
|
|
735
|
+
},
|
|
736
|
+
cancelNotification: function(id) {
|
|
737
|
+
return Promise.resolve().then(function() { return window.craft.notifications.cancel(id); });
|
|
738
|
+
},
|
|
739
|
+
cancelAllNotifications: function() {
|
|
740
|
+
return Promise.resolve().then(function() { return window.craft.notifications.cancelAll(); });
|
|
741
|
+
},
|
|
742
|
+
getPendingNotifications: function() {
|
|
743
|
+
return Promise.reject(new Error('Pending notification queries are unavailable on Android'));
|
|
744
|
+
},
|
|
745
|
+
getProducts: function(productIds) {
|
|
746
|
+
return window.craft.iap.getProducts(productIds);
|
|
747
|
+
},
|
|
748
|
+
purchase: function(productId) {
|
|
749
|
+
return window.craft.iap.purchase(productId);
|
|
750
|
+
},
|
|
751
|
+
restorePurchases: function() {
|
|
752
|
+
return window.craft.iap.restore();
|
|
753
|
+
},
|
|
754
|
+
signInWithApple: function() {
|
|
755
|
+
return Promise.reject(new Error('Sign in with Apple is unavailable on Android'));
|
|
756
|
+
},
|
|
757
|
+
startBluetoothScan: function() {
|
|
758
|
+
return window.craft.bluetooth.startScan();
|
|
759
|
+
},
|
|
760
|
+
stopBluetoothScan: function() {
|
|
761
|
+
return Promise.resolve().then(function() { return window.craft.bluetooth.stopScan(); });
|
|
762
|
+
},
|
|
763
|
+
requestHealthAuthorization: function() {
|
|
764
|
+
return Promise.reject(new Error('HealthKit is unavailable on Android'));
|
|
765
|
+
},
|
|
766
|
+
getHealthData: function() {
|
|
767
|
+
return Promise.reject(new Error('HealthKit is unavailable on Android'));
|
|
768
|
+
},
|
|
769
|
+
requestFitnessAuthorization: function(types) {
|
|
770
|
+
return window.craft.health.requestAuthorization(types || [
|
|
771
|
+
'steps', 'heartRate', 'activeEnergy', 'distance', 'workouts'
|
|
772
|
+
]);
|
|
773
|
+
},
|
|
774
|
+
getFitnessData: function(type, startDate, endDate) {
|
|
775
|
+
var start = startDate instanceof Date ? startDate.getTime() : startDate;
|
|
776
|
+
var end = endDate instanceof Date ? endDate.getTime() : endDate;
|
|
777
|
+
return window.craft.health.getData(type, {startDate: start, endDate: end});
|
|
778
|
+
},
|
|
779
|
+
|
|
627
780
|
// Screen Capture
|
|
628
781
|
takeScreenshot: function() {
|
|
629
|
-
return
|
|
630
|
-
window._craftScreenshotResolve = resolve;
|
|
631
|
-
window._craftScreenshotReject = reject;
|
|
782
|
+
return window.__craftPromise('screenshot', '_craftScreenshotResolve', '_craftScreenshotReject', function() {
|
|
632
783
|
CraftAndroid.takeScreenshot();
|
|
633
784
|
});
|
|
634
785
|
},
|
|
@@ -636,17 +787,13 @@ class CraftBridge(
|
|
|
636
787
|
// Background Tasks
|
|
637
788
|
backgroundTask: {
|
|
638
789
|
register: function(taskId) {
|
|
639
|
-
return
|
|
640
|
-
window._craftBgTaskResolve = resolve;
|
|
641
|
-
window._craftBgTaskReject = reject;
|
|
790
|
+
return window.__craftPromise('background task', '_craftBgTaskResolve', '_craftBgTaskReject', function() {
|
|
642
791
|
CraftAndroid.registerBackgroundTask(taskId);
|
|
643
792
|
});
|
|
644
793
|
},
|
|
645
794
|
schedule: function(taskId, options) {
|
|
646
795
|
options = options || {};
|
|
647
|
-
return
|
|
648
|
-
window._craftBgTaskResolve = resolve;
|
|
649
|
-
window._craftBgTaskReject = reject;
|
|
796
|
+
return window.__craftPromise('background task', '_craftBgTaskResolve', '_craftBgTaskReject', function() {
|
|
650
797
|
CraftAndroid.scheduleBackgroundTask(
|
|
651
798
|
taskId,
|
|
652
799
|
options.delay || 900,
|
|
@@ -656,16 +803,12 @@ class CraftBridge(
|
|
|
656
803
|
});
|
|
657
804
|
},
|
|
658
805
|
cancel: function(taskId) {
|
|
659
|
-
return
|
|
660
|
-
window._craftBgTaskResolve = resolve;
|
|
661
|
-
window._craftBgTaskReject = reject;
|
|
806
|
+
return window.__craftPromise('background task', '_craftBgTaskResolve', '_craftBgTaskReject', function() {
|
|
662
807
|
CraftAndroid.cancelBackgroundTask(taskId);
|
|
663
808
|
});
|
|
664
809
|
},
|
|
665
810
|
cancelAll: function() {
|
|
666
|
-
return
|
|
667
|
-
window._craftBgTaskResolve = resolve;
|
|
668
|
-
window._craftBgTaskReject = reject;
|
|
811
|
+
return window.__craftPromise('background task', '_craftBgTaskResolve', '_craftBgTaskReject', function() {
|
|
669
812
|
CraftAndroid.cancelAllBackgroundTasks();
|
|
670
813
|
});
|
|
671
814
|
}
|
|
@@ -673,16 +816,12 @@ class CraftBridge(
|
|
|
673
816
|
|
|
674
817
|
// PDF Viewer
|
|
675
818
|
openPDF: function(source, page) {
|
|
676
|
-
return
|
|
677
|
-
window._craftPDFResolve = resolve;
|
|
678
|
-
window._craftPDFReject = reject;
|
|
819
|
+
return window.__craftPromise('PDF', '_craftPDFResolve', '_craftPDFReject', function() {
|
|
679
820
|
CraftAndroid.openPDF(source, page || 0);
|
|
680
821
|
});
|
|
681
822
|
},
|
|
682
823
|
closePDF: function() {
|
|
683
|
-
return
|
|
684
|
-
window._craftPDFResolve = resolve;
|
|
685
|
-
window._craftPDFReject = reject;
|
|
824
|
+
return window.__craftPromise('PDF', '_craftPDFResolve', '_craftPDFReject', function() {
|
|
686
825
|
CraftAndroid.closePDF();
|
|
687
826
|
});
|
|
688
827
|
},
|
|
@@ -690,9 +829,7 @@ class CraftBridge(
|
|
|
690
829
|
// Contacts Picker
|
|
691
830
|
pickContact: function(options) {
|
|
692
831
|
options = options || {};
|
|
693
|
-
return
|
|
694
|
-
window._craftPickContactResolve = resolve;
|
|
695
|
-
window._craftPickContactReject = reject;
|
|
832
|
+
return window.__craftPromise('contact picker', '_craftPickContactResolve', '_craftPickContactReject', function() {
|
|
696
833
|
CraftAndroid.pickContact(options.multiple || false);
|
|
697
834
|
});
|
|
698
835
|
},
|
|
@@ -700,16 +837,12 @@ class CraftBridge(
|
|
|
700
837
|
// App Shortcuts
|
|
701
838
|
shortcuts: {
|
|
702
839
|
set: function(shortcuts) {
|
|
703
|
-
return
|
|
704
|
-
window._craftShortcutsResolve = resolve;
|
|
705
|
-
window._craftShortcutsReject = reject;
|
|
840
|
+
return window.__craftPromise('shortcuts', '_craftShortcutsResolve', '_craftShortcutsReject', function() {
|
|
706
841
|
CraftAndroid.setShortcuts(JSON.stringify(shortcuts));
|
|
707
842
|
});
|
|
708
843
|
},
|
|
709
844
|
clear: function() {
|
|
710
|
-
return
|
|
711
|
-
window._craftShortcutsResolve = resolve;
|
|
712
|
-
window._craftShortcutsReject = reject;
|
|
845
|
+
return window.__craftPromise('shortcuts', '_craftShortcutsResolve', '_craftShortcutsReject', function() {
|
|
713
846
|
CraftAndroid.clearShortcuts();
|
|
714
847
|
});
|
|
715
848
|
},
|
|
@@ -721,23 +854,17 @@ class CraftBridge(
|
|
|
721
854
|
// Shared Preferences (Android equivalent of Keychain Sharing)
|
|
722
855
|
sharedKeychain: {
|
|
723
856
|
set: function(key, value, group) {
|
|
724
|
-
return
|
|
725
|
-
window._craftSharedKeychainResolve = resolve;
|
|
726
|
-
window._craftSharedKeychainReject = reject;
|
|
857
|
+
return window.__craftPromise('shared keychain', '_craftSharedKeychainResolve', '_craftSharedKeychainReject', function() {
|
|
727
858
|
CraftAndroid.setSharedItem(key, value, group || '');
|
|
728
859
|
});
|
|
729
860
|
},
|
|
730
861
|
get: function(key, group) {
|
|
731
|
-
return
|
|
732
|
-
window._craftSharedKeychainResolve = resolve;
|
|
733
|
-
window._craftSharedKeychainReject = reject;
|
|
862
|
+
return window.__craftPromise('shared keychain', '_craftSharedKeychainResolve', '_craftSharedKeychainReject', function() {
|
|
734
863
|
CraftAndroid.getSharedItem(key, group || '');
|
|
735
864
|
});
|
|
736
865
|
},
|
|
737
866
|
remove: function(key, group) {
|
|
738
|
-
return
|
|
739
|
-
window._craftSharedKeychainResolve = resolve;
|
|
740
|
-
window._craftSharedKeychainReject = reject;
|
|
867
|
+
return window.__craftPromise('shared keychain', '_craftSharedKeychainResolve', '_craftSharedKeychainReject', function() {
|
|
741
868
|
CraftAndroid.removeSharedItem(key, group || '');
|
|
742
869
|
});
|
|
743
870
|
}
|
|
@@ -746,30 +873,22 @@ class CraftBridge(
|
|
|
746
873
|
// Local Auth Persistence
|
|
747
874
|
authPersistence: {
|
|
748
875
|
enable: function(duration) {
|
|
749
|
-
return
|
|
750
|
-
window._craftAuthPersistResolve = resolve;
|
|
751
|
-
window._craftAuthPersistReject = reject;
|
|
876
|
+
return window.__craftPromise('auth persistence', '_craftAuthPersistResolve', '_craftAuthPersistReject', function() {
|
|
752
877
|
CraftAndroid.setAuthPersistence(true, duration || 300);
|
|
753
878
|
});
|
|
754
879
|
},
|
|
755
880
|
disable: function() {
|
|
756
|
-
return
|
|
757
|
-
window._craftAuthPersistResolve = resolve;
|
|
758
|
-
window._craftAuthPersistReject = reject;
|
|
881
|
+
return window.__craftPromise('auth persistence', '_craftAuthPersistResolve', '_craftAuthPersistReject', function() {
|
|
759
882
|
CraftAndroid.setAuthPersistence(false, 0);
|
|
760
883
|
});
|
|
761
884
|
},
|
|
762
885
|
check: function() {
|
|
763
|
-
return
|
|
764
|
-
window._craftAuthPersistResolve = resolve;
|
|
765
|
-
window._craftAuthPersistReject = reject;
|
|
886
|
+
return window.__craftPromise('auth persistence', '_craftAuthPersistResolve', '_craftAuthPersistReject', function() {
|
|
766
887
|
CraftAndroid.checkAuthPersistence();
|
|
767
888
|
});
|
|
768
889
|
},
|
|
769
890
|
clear: function() {
|
|
770
|
-
return
|
|
771
|
-
window._craftAuthPersistResolve = resolve;
|
|
772
|
-
window._craftAuthPersistReject = reject;
|
|
891
|
+
return window.__craftPromise('auth persistence', '_craftAuthPersistResolve', '_craftAuthPersistReject', function() {
|
|
773
892
|
CraftAndroid.clearAuthPersistence();
|
|
774
893
|
});
|
|
775
894
|
}
|
|
@@ -778,76 +897,101 @@ class CraftBridge(
|
|
|
778
897
|
// AR (ARCore) - Note: Full ARCore requires native Activity integration
|
|
779
898
|
ar: {
|
|
780
899
|
start: function(options) {
|
|
781
|
-
return
|
|
782
|
-
window._craftARResolve = resolve;
|
|
783
|
-
window._craftARReject = reject;
|
|
900
|
+
return window.__craftPromise('AR', '_craftARResolve', '_craftARReject', function() {
|
|
784
901
|
CraftAndroid.startAR(JSON.stringify(options || {}));
|
|
785
902
|
});
|
|
786
903
|
},
|
|
787
904
|
stop: function() {
|
|
788
|
-
return
|
|
789
|
-
window._craftARResolve = resolve;
|
|
790
|
-
window._craftARReject = reject;
|
|
905
|
+
return window.__craftPromise('AR', '_craftARResolve', '_craftARReject', function() {
|
|
791
906
|
CraftAndroid.stopAR();
|
|
792
907
|
});
|
|
793
908
|
},
|
|
794
909
|
placeObject: function(model, position) {
|
|
795
|
-
return
|
|
796
|
-
window._craftARResolve = resolve;
|
|
797
|
-
window._craftARReject = reject;
|
|
910
|
+
return window.__craftPromise('AR', '_craftARResolve', '_craftARReject', function() {
|
|
798
911
|
CraftAndroid.placeARObject(model, JSON.stringify(position || {}));
|
|
799
912
|
});
|
|
800
913
|
},
|
|
801
914
|
removeObject: function(objectId) {
|
|
802
|
-
return
|
|
803
|
-
window._craftARResolve = resolve;
|
|
804
|
-
window._craftARReject = reject;
|
|
915
|
+
return window.__craftPromise('AR', '_craftARResolve', '_craftARReject', function() {
|
|
805
916
|
CraftAndroid.removeARObject(objectId);
|
|
806
917
|
});
|
|
807
918
|
},
|
|
808
919
|
getPlanes: function() {
|
|
809
|
-
return
|
|
810
|
-
window._craftARResolve = resolve;
|
|
811
|
-
window._craftARReject = reject;
|
|
920
|
+
return window.__craftPromise('AR', '_craftARResolve', '_craftARReject', function() {
|
|
812
921
|
CraftAndroid.getARPlanes();
|
|
813
922
|
});
|
|
814
923
|
},
|
|
815
924
|
onPlaneDetected: function(callback) {
|
|
816
|
-
|
|
925
|
+
throw new Error('craft.ar.onPlaneDetected is unavailable because ARCore requires native Activity integration');
|
|
817
926
|
}
|
|
818
927
|
},
|
|
819
928
|
|
|
820
929
|
// ML (ML Kit)
|
|
821
930
|
ml: {
|
|
822
931
|
classifyImage: function(imageBase64) {
|
|
823
|
-
return
|
|
824
|
-
window._craftMLResolve = resolve;
|
|
825
|
-
window._craftMLReject = reject;
|
|
932
|
+
return window.__craftPromise('ML', '_craftMLResolve', '_craftMLReject', function() {
|
|
826
933
|
CraftAndroid.classifyImage(imageBase64);
|
|
827
934
|
});
|
|
828
935
|
},
|
|
829
936
|
detectObjects: function(imageBase64) {
|
|
830
|
-
return
|
|
831
|
-
window._craftMLResolve = resolve;
|
|
832
|
-
window._craftMLReject = reject;
|
|
937
|
+
return window.__craftPromise('ML', '_craftMLResolve', '_craftMLReject', function() {
|
|
833
938
|
CraftAndroid.detectObjects(imageBase64);
|
|
834
939
|
});
|
|
835
940
|
},
|
|
836
941
|
recognizeText: function(imageBase64) {
|
|
837
|
-
return
|
|
838
|
-
window._craftMLResolve = resolve;
|
|
839
|
-
window._craftMLReject = reject;
|
|
942
|
+
return window.__craftPromise('ML', '_craftMLResolve', '_craftMLReject', function() {
|
|
840
943
|
CraftAndroid.recognizeText(imageBase64);
|
|
841
944
|
});
|
|
842
945
|
}
|
|
843
946
|
},
|
|
844
947
|
|
|
948
|
+
// Android home-screen widgets
|
|
949
|
+
widgets: {
|
|
950
|
+
updateWidget: function(widgetId, data) {
|
|
951
|
+
var payload = data === undefined ? (widgetId || {}) : Object.assign({}, data || {}, {widgetId: widgetId});
|
|
952
|
+
return window.__craftPromise('widgets', '_craftWidgetResolve', '_craftWidgetReject', function() {
|
|
953
|
+
CraftAndroid.updateWidget(JSON.stringify(payload));
|
|
954
|
+
});
|
|
955
|
+
},
|
|
956
|
+
updateAllWidgets: function(widgetClass, data) {
|
|
957
|
+
var payload = Object.assign({}, data || {}, {widgetClass: widgetClass});
|
|
958
|
+
return window.__craftPromise('widgets', '_craftWidgetResolve', '_craftWidgetReject', function() {
|
|
959
|
+
CraftAndroid.updateWidget(JSON.stringify(payload));
|
|
960
|
+
});
|
|
961
|
+
},
|
|
962
|
+
reload: function() {
|
|
963
|
+
return window.__craftPromise('widgets', '_craftWidgetResolve', '_craftWidgetReject', function() {
|
|
964
|
+
CraftAndroid.reloadWidgets();
|
|
965
|
+
});
|
|
966
|
+
},
|
|
967
|
+
getActiveWidgets: function() { return Promise.resolve([]); },
|
|
968
|
+
requestPin: function() { return Promise.resolve(false); },
|
|
969
|
+
isSupported: function() { return Promise.resolve(false); }
|
|
970
|
+
},
|
|
971
|
+
|
|
972
|
+
// Wear OS companion connectivity
|
|
973
|
+
watch: {
|
|
974
|
+
send: function(message) {
|
|
975
|
+
return window.__craftPromise('watch', '_craftWatchResolve', '_craftWatchReject', function() {
|
|
976
|
+
CraftAndroid.sendToWatch(JSON.stringify(message));
|
|
977
|
+
});
|
|
978
|
+
},
|
|
979
|
+
updateContext: function(context) {
|
|
980
|
+
return window.__craftPromise('watch', '_craftWatchResolve', '_craftWatchReject', function() {
|
|
981
|
+
CraftAndroid.updateWatchContext(JSON.stringify(context));
|
|
982
|
+
});
|
|
983
|
+
},
|
|
984
|
+
isReachable: function() {
|
|
985
|
+
return Promise.resolve().then(function() {
|
|
986
|
+
return {reachable: CraftAndroid.isWatchReachable()};
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
},
|
|
990
|
+
|
|
845
991
|
// Deep Links
|
|
846
992
|
deepLinks: {
|
|
847
993
|
getInitialURL: function() {
|
|
848
|
-
return
|
|
849
|
-
window._craftDeepLinkResolve = resolve;
|
|
850
|
-
window._craftDeepLinkReject = reject;
|
|
994
|
+
return window.__craftPromise('initial URL', '_craftDeepLinkResolve', '_craftDeepLinkReject', function() {
|
|
851
995
|
CraftAndroid.getInitialURL();
|
|
852
996
|
});
|
|
853
997
|
},
|
|
@@ -932,38 +1076,28 @@ class CraftBridge(
|
|
|
932
1076
|
ota: {
|
|
933
1077
|
_config: null,
|
|
934
1078
|
_status: 'idle',
|
|
935
|
-
_progressCallbacks: [],
|
|
936
|
-
_statusCallbacks: [],
|
|
937
1079
|
|
|
938
1080
|
configure: function(options) {
|
|
939
1081
|
this._config = options;
|
|
940
1082
|
CraftAndroid.otaConfigure(JSON.stringify(options));
|
|
941
1083
|
},
|
|
942
1084
|
checkForUpdate: function() {
|
|
943
|
-
return
|
|
944
|
-
window._craftOTACheckResolve = resolve;
|
|
945
|
-
window._craftOTACheckReject = reject;
|
|
1085
|
+
return window.__craftPromise('OTA check', '_craftOTACheckResolve', '_craftOTACheckReject', function() {
|
|
946
1086
|
CraftAndroid.otaCheckForUpdate();
|
|
947
1087
|
});
|
|
948
1088
|
},
|
|
949
1089
|
downloadUpdate: function(options) {
|
|
950
|
-
return
|
|
951
|
-
window._craftOTADownloadResolve = resolve;
|
|
952
|
-
window._craftOTADownloadReject = reject;
|
|
1090
|
+
return window.__craftPromise('OTA download', '_craftOTADownloadResolve', '_craftOTADownloadReject', function() {
|
|
953
1091
|
CraftAndroid.otaDownloadUpdate(JSON.stringify(options || {}));
|
|
954
1092
|
});
|
|
955
1093
|
},
|
|
956
1094
|
applyUpdate: function() {
|
|
957
|
-
return
|
|
958
|
-
window._craftOTAApplyResolve = resolve;
|
|
959
|
-
window._craftOTAApplyReject = reject;
|
|
1095
|
+
return window.__craftPromise('OTA apply', '_craftOTAApplyResolve', '_craftOTAApplyReject', function() {
|
|
960
1096
|
CraftAndroid.otaApplyUpdate();
|
|
961
1097
|
});
|
|
962
1098
|
},
|
|
963
1099
|
rollback: function() {
|
|
964
|
-
return
|
|
965
|
-
window._craftOTARollbackResolve = resolve;
|
|
966
|
-
window._craftOTARollbackReject = reject;
|
|
1100
|
+
return window.__craftPromise('OTA rollback', '_craftOTARollbackResolve', '_craftOTARollbackReject', function() {
|
|
967
1101
|
CraftAndroid.otaRollback();
|
|
968
1102
|
});
|
|
969
1103
|
},
|
|
@@ -973,12 +1107,10 @@ class CraftBridge(
|
|
|
973
1107
|
catch(e) { return { version: '1.0.0', buildNumber: 1, hash: '', isOriginal: true, installedAt: '' }; }
|
|
974
1108
|
},
|
|
975
1109
|
onProgress: function(callback) {
|
|
976
|
-
|
|
977
|
-
window.addEventListener('craftOTAProgress', function(e) { callback(e.detail); });
|
|
1110
|
+
throw new Error('craft.ota.onProgress is not implemented on this platform');
|
|
978
1111
|
},
|
|
979
1112
|
onStatusChange: function(callback) {
|
|
980
|
-
|
|
981
|
-
window.addEventListener('craftOTAStatus', function(e) { callback(e.detail.status); });
|
|
1113
|
+
throw new Error('craft.ota.onStatusChange is not implemented on this platform');
|
|
982
1114
|
}
|
|
983
1115
|
},
|
|
984
1116
|
|
|
@@ -1222,14 +1354,14 @@ class CraftBridge(
|
|
|
1222
1354
|
var legacySecureStore = craft.secureStore;
|
|
1223
1355
|
craft.contractVersion = '1.0.0';
|
|
1224
1356
|
craft.device = {
|
|
1225
|
-
getInfo: function() { return Promise.resolve(craft.getDeviceInfo()); },
|
|
1357
|
+
getInfo: function() { return Promise.resolve().then(function() { return craft.getDeviceInfo(); }); },
|
|
1226
1358
|
getCapabilities: function() { return Promise.resolve(Object.assign({}, craft.capabilities)); }
|
|
1227
1359
|
};
|
|
1228
1360
|
craft.haptics = {
|
|
1229
|
-
impact: function(style) { craft.haptic(style || 'medium');
|
|
1230
|
-
notification: function(type) { craft.haptic(type === 'error' ? 'heavy' : 'light');
|
|
1231
|
-
selection: function() { craft.haptic('selection');
|
|
1232
|
-
vibrate: function(pattern) { craft.vibrate(pattern || []);
|
|
1361
|
+
impact: function(style) { return Promise.resolve().then(function() { craft.haptic(style || 'medium'); }); },
|
|
1362
|
+
notification: function(type) { return Promise.resolve().then(function() { craft.haptic(type === 'error' ? 'heavy' : 'light'); }); },
|
|
1363
|
+
selection: function() { return Promise.resolve().then(function() { craft.haptic('selection'); }); },
|
|
1364
|
+
vibrate: function(pattern) { return Promise.resolve().then(function() { craft.vibrate(pattern || []); }); }
|
|
1233
1365
|
};
|
|
1234
1366
|
craft.camera = {
|
|
1235
1367
|
takePicture: function() { return legacyOpenCamera().then(normalizePhoto); },
|
|
@@ -1243,26 +1375,77 @@ class CraftBridge(
|
|
|
1243
1375
|
authenticate: function(reason) { return legacyAuthenticate(reason); }
|
|
1244
1376
|
};
|
|
1245
1377
|
craft.secureStorage = {
|
|
1246
|
-
set: function(key, value) { return Promise.resolve(legacySecureStore.set(key, value)
|
|
1247
|
-
get: function(key) { return Promise.resolve(legacySecureStore.get(key)); },
|
|
1248
|
-
delete: function(key) { return Promise.resolve(
|
|
1249
|
-
clear: function() { return Promise.resolve(
|
|
1378
|
+
set: function(key, value) { return Promise.resolve().then(function() { legacySecureStore.set(key, value); }); },
|
|
1379
|
+
get: function(key) { return Promise.resolve().then(function() { return legacySecureStore.get(key); }); },
|
|
1380
|
+
delete: function(key) { return Promise.resolve().then(function() { legacySecureStore.remove(key); }); },
|
|
1381
|
+
clear: function() { return Promise.resolve().then(function() { CraftAndroid.secureClear(); }); }
|
|
1382
|
+
};
|
|
1383
|
+
var permissionRequestSequence = 0;
|
|
1384
|
+
var permissionRequests = Object.create(null);
|
|
1385
|
+
var permissionRuntimeClosed = false;
|
|
1386
|
+
window.__craftPermissionResult = function(id, status) {
|
|
1387
|
+
if (permissionRuntimeClosed) return;
|
|
1388
|
+
var pending = permissionRequests[id];
|
|
1389
|
+
if (!pending) return;
|
|
1390
|
+
delete permissionRequests[id];
|
|
1391
|
+
clearTimeout(pending.timeout);
|
|
1392
|
+
pending.resolve(status || 'undetermined');
|
|
1393
|
+
};
|
|
1394
|
+
window.__craftRejectPermissionRequests = function(message) {
|
|
1395
|
+
permissionRuntimeClosed = true;
|
|
1396
|
+
Object.keys(permissionRequests).forEach(function(id) {
|
|
1397
|
+
var pending = permissionRequests[id];
|
|
1398
|
+
delete permissionRequests[id];
|
|
1399
|
+
clearTimeout(pending.timeout);
|
|
1400
|
+
pending.reject(new Error(message || 'Android bridge closed'));
|
|
1401
|
+
});
|
|
1402
|
+
};
|
|
1403
|
+
craft.permissions = {
|
|
1404
|
+
check: function(permission) {
|
|
1405
|
+
return Promise.resolve().then(function() { return CraftAndroid.checkPermission(String(permission)); });
|
|
1406
|
+
},
|
|
1407
|
+
request: function(permission) {
|
|
1408
|
+
if (permissionRuntimeClosed) {
|
|
1409
|
+
return Promise.reject(new Error('Android bridge is closed'));
|
|
1410
|
+
}
|
|
1411
|
+
return new Promise(function(resolve, reject) {
|
|
1412
|
+
permissionRequestSequence += 1;
|
|
1413
|
+
var id = permissionRequestSequence;
|
|
1414
|
+
var timeout = setTimeout(function() {
|
|
1415
|
+
delete permissionRequests[id];
|
|
1416
|
+
reject(new Error('Craft permission request timed out'));
|
|
1417
|
+
}, 30000);
|
|
1418
|
+
permissionRequests[id] = {resolve: resolve, timeout: timeout};
|
|
1419
|
+
try {
|
|
1420
|
+
CraftAndroid.requestPermission(String(permission), id);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
clearTimeout(timeout);
|
|
1423
|
+
delete permissionRequests[id];
|
|
1424
|
+
reject(error);
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
},
|
|
1428
|
+
openSettings: function() {
|
|
1429
|
+
return Promise.resolve().then(function() { CraftAndroid.openPermissionSettings(); });
|
|
1430
|
+
}
|
|
1250
1431
|
};
|
|
1251
1432
|
craft.location = {
|
|
1252
1433
|
getCurrentPosition: craft.getCurrentPosition,
|
|
1253
1434
|
watchPosition: craft.watchPosition,
|
|
1254
1435
|
clearWatch: craft.clearWatch,
|
|
1255
|
-
startRecording: function(options) { return Promise.resolve(JSON.parse(CraftAndroid.startLocationRecording(JSON.stringify(options || {})))); },
|
|
1256
|
-
pauseRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.pauseLocationRecording())); },
|
|
1257
|
-
resumeRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.resumeLocationRecording())); },
|
|
1258
|
-
stopRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.stopLocationRecording())); },
|
|
1259
|
-
getRecordingState: function() { return Promise.resolve(JSON.parse(CraftAndroid.getLocationRecordingState())); },
|
|
1260
|
-
readRecording: function() { return Promise.resolve(JSON.parse(CraftAndroid.readLocationRecording())); }
|
|
1436
|
+
startRecording: function(options) { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.startLocationRecording(JSON.stringify(options || {}))); }); },
|
|
1437
|
+
pauseRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.pauseLocationRecording()); }); },
|
|
1438
|
+
resumeRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.resumeLocationRecording()); }); },
|
|
1439
|
+
stopRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.stopLocationRecording()); }); },
|
|
1440
|
+
getRecordingState: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.getLocationRecordingState()); }); },
|
|
1441
|
+
readRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.readLocationRecording()); }); }
|
|
1261
1442
|
};
|
|
1262
1443
|
var shareApi = function(text, title) { return legacyShare(text, title); };
|
|
1263
1444
|
shareApi.share = function(options) {
|
|
1264
1445
|
options = options || {};
|
|
1265
|
-
return Promise.resolve(
|
|
1446
|
+
return Promise.resolve().then(function() {
|
|
1447
|
+
return legacyShare([options.text, options.url].filter(Boolean).join(' '), options.title || '');
|
|
1448
|
+
});
|
|
1266
1449
|
};
|
|
1267
1450
|
craft.share = shareApi;
|
|
1268
1451
|
craft.lifecycle = {
|
|
@@ -1287,14 +1470,27 @@ class CraftBridge(
|
|
|
1287
1470
|
""".trimIndent()
|
|
1288
1471
|
|
|
1289
1472
|
activity.runOnUiThread {
|
|
1290
|
-
|
|
1473
|
+
if (closed) return@runOnUiThread
|
|
1474
|
+
webView.evaluateJavascript(script) {
|
|
1475
|
+
if (closed) return@evaluateJavascript
|
|
1476
|
+
bridgeReady = true
|
|
1477
|
+
val events = pendingEvents.toList()
|
|
1478
|
+
pendingEvents.clear()
|
|
1479
|
+
events.forEach { (name, data) -> sendEvent(name, data) }
|
|
1480
|
+
}
|
|
1291
1481
|
}
|
|
1292
1482
|
}
|
|
1293
1483
|
|
|
1484
|
+
fun markBridgeLoading() {
|
|
1485
|
+
bridgeReady = false
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1294
1488
|
// ==================== Haptics ====================
|
|
1295
1489
|
|
|
1296
1490
|
@JavascriptInterface
|
|
1297
1491
|
fun haptic(style: String) {
|
|
1492
|
+
if (CraftNative.haptic(activity, style)) return
|
|
1493
|
+
|
|
1298
1494
|
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
1299
1495
|
val vibratorManager = activity.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
|
1300
1496
|
vibratorManager.defaultVibrator
|
|
@@ -1325,15 +1521,18 @@ class CraftBridge(
|
|
|
1325
1521
|
|
|
1326
1522
|
@JavascriptInterface
|
|
1327
1523
|
fun startListening() {
|
|
1524
|
+
if (CraftNative.startListening(activity)) return
|
|
1525
|
+
|
|
1328
1526
|
if (!SpeechRecognizer.isRecognitionAvailable(activity)) {
|
|
1329
1527
|
sendEvent("craftSpeechError", mapOf("error" to "Speech recognition not available"))
|
|
1330
1528
|
return
|
|
1331
1529
|
}
|
|
1332
1530
|
|
|
1333
1531
|
activity.runOnUiThread {
|
|
1532
|
+
if (closed) return@runOnUiThread
|
|
1334
1533
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
|
|
1335
1534
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
1336
|
-
|
|
1535
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.RECORD_AUDIO), 100)
|
|
1337
1536
|
return@runOnUiThread
|
|
1338
1537
|
}
|
|
1339
1538
|
|
|
@@ -1399,6 +1598,8 @@ class CraftBridge(
|
|
|
1399
1598
|
|
|
1400
1599
|
@JavascriptInterface
|
|
1401
1600
|
fun stopListening() {
|
|
1601
|
+
if (CraftNative.stopListening(activity)) return
|
|
1602
|
+
|
|
1402
1603
|
activity.runOnUiThread {
|
|
1403
1604
|
speechRecognizer?.stopListening()
|
|
1404
1605
|
speechRecognizer?.destroy()
|
|
@@ -1411,6 +1612,8 @@ class CraftBridge(
|
|
|
1411
1612
|
|
|
1412
1613
|
@JavascriptInterface
|
|
1413
1614
|
fun share(text: String, title: String) {
|
|
1615
|
+
if (CraftNative.share(activity, text, title)) return
|
|
1616
|
+
|
|
1414
1617
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
1415
1618
|
type = "text/plain"
|
|
1416
1619
|
putExtra(Intent.EXTRA_TEXT, text)
|
|
@@ -1427,45 +1630,150 @@ class CraftBridge(
|
|
|
1427
1630
|
fun openCamera() {
|
|
1428
1631
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
|
|
1429
1632
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
1430
|
-
|
|
1633
|
+
rejectMediaPromise("_craftCamera", "Camera permission is required; retry after granting it")
|
|
1634
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.CAMERA), 101)
|
|
1431
1635
|
return
|
|
1432
1636
|
}
|
|
1433
1637
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1638
|
+
// The Activity result still returns to handleImageResult below; Zig
|
|
1639
|
+
// only launches it, using the same request code that routes it here.
|
|
1640
|
+
if (CraftNative.openCamera(activity)) return
|
|
1641
|
+
|
|
1642
|
+
try {
|
|
1643
|
+
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
|
1644
|
+
activity.startActivityForResult(intent, REQUEST_CAMERA)
|
|
1645
|
+
} catch (error: Exception) {
|
|
1646
|
+
rejectMediaPromise("_craftCamera", error.message ?: "Camera could not be opened")
|
|
1647
|
+
}
|
|
1436
1648
|
}
|
|
1437
1649
|
|
|
1438
1650
|
@JavascriptInterface
|
|
1439
1651
|
fun pickImage() {
|
|
1440
|
-
|
|
1441
|
-
|
|
1652
|
+
if (CraftNative.pickImage(activity)) return
|
|
1653
|
+
|
|
1654
|
+
try {
|
|
1655
|
+
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
|
|
1656
|
+
activity.startActivityForResult(intent, REQUEST_GALLERY)
|
|
1657
|
+
} catch (error: Exception) {
|
|
1658
|
+
rejectMediaPromise("_craftGallery", error.message ?: "Image picker could not be opened")
|
|
1659
|
+
}
|
|
1442
1660
|
}
|
|
1443
1661
|
|
|
1444
1662
|
fun handleImageResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
1663
|
+
if (closed) return
|
|
1664
|
+
val promise = if (requestCode == REQUEST_CAMERA) "_craftCamera" else "_craftGallery"
|
|
1445
1665
|
if (resultCode != Activity.RESULT_OK) {
|
|
1446
|
-
|
|
1447
|
-
activity.runOnUiThread {
|
|
1448
|
-
webView.evaluateJavascript("window.$event && window.$event('Cancelled')", null)
|
|
1449
|
-
}
|
|
1666
|
+
rejectMediaPromise(promise, "Cancelled")
|
|
1450
1667
|
return
|
|
1451
1668
|
}
|
|
1452
1669
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1670
|
+
if (requestCode == REQUEST_CAMERA) {
|
|
1671
|
+
val bitmap = data?.extras?.get("data") as? Bitmap
|
|
1672
|
+
if (bitmap == null) {
|
|
1673
|
+
rejectMediaPromise(promise, "Camera returned no image")
|
|
1674
|
+
} else {
|
|
1675
|
+
resolveMediaPromise(promise, "data:image/png;base64,${bitmapToBase64(bitmap)}")
|
|
1458
1676
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1677
|
+
return
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
val uri = data?.data
|
|
1681
|
+
if (uri == null) {
|
|
1682
|
+
rejectMediaPromise(promise, "Gallery returned no image")
|
|
1683
|
+
return
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
Thread {
|
|
1687
|
+
try {
|
|
1688
|
+
val bytes = activity.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
|
1689
|
+
?: throw IllegalStateException("Selected image could not be opened")
|
|
1690
|
+
val mimeType = activity.contentResolver.getType(uri) ?: "image/jpeg"
|
|
1691
|
+
val encoded = Base64.encodeToString(bytes, Base64.NO_WRAP)
|
|
1692
|
+
resolveMediaPromise(promise, "data:$mimeType;base64,$encoded")
|
|
1693
|
+
} catch (e: Exception) {
|
|
1694
|
+
rejectMediaPromise(promise, e.message ?: "Selected image could not be read")
|
|
1461
1695
|
}
|
|
1462
|
-
|
|
1696
|
+
}.start()
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
private fun resolveMediaPromise(promise: String, value: String) {
|
|
1700
|
+
evaluatePromiseJavascript(
|
|
1701
|
+
"window.${promise}Resolve && window.${promise}Resolve(${jsQuote(value)})"
|
|
1702
|
+
)
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
private fun rejectMediaPromise(promise: String, message: String) {
|
|
1706
|
+
evaluatePromiseJavascript(
|
|
1707
|
+
"window.${promise}Reject && window.${promise}Reject(${jsQuote(message)})"
|
|
1708
|
+
)
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
private fun handleFilePickerResult(resultCode: Int, data: Intent?) {
|
|
1712
|
+
if (closed) return
|
|
1713
|
+
val uri = data?.data
|
|
1714
|
+
if (resultCode != Activity.RESULT_OK || uri == null) {
|
|
1715
|
+
rejectFilePicker("Cancelled")
|
|
1716
|
+
return
|
|
1463
1717
|
}
|
|
1464
1718
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1719
|
+
Thread {
|
|
1720
|
+
try {
|
|
1721
|
+
val bytes = activity.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
|
1722
|
+
?: throw IllegalStateException("Selected file could not be opened")
|
|
1723
|
+
var name = uri.lastPathSegment ?: "file"
|
|
1724
|
+
activity.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
|
|
1725
|
+
if (cursor.moveToFirst()) {
|
|
1726
|
+
name = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
val result = JSONObject().apply {
|
|
1730
|
+
put("name", name)
|
|
1731
|
+
put("data", Base64.encodeToString(bytes, Base64.NO_WRAP))
|
|
1732
|
+
put("mimeType", activity.contentResolver.getType(uri) ?: "application/octet-stream")
|
|
1733
|
+
}
|
|
1734
|
+
evaluatePromiseJavascript("window._craftFileResolve && window._craftFileResolve($result)")
|
|
1735
|
+
} catch (e: Exception) {
|
|
1736
|
+
rejectFilePicker(e.message ?: "Selected file could not be read")
|
|
1737
|
+
}
|
|
1738
|
+
}.start()
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
private fun rejectFilePicker(message: String) {
|
|
1742
|
+
evaluatePromiseJavascript(
|
|
1743
|
+
"window._craftFileReject && window._craftFileReject(${jsQuote(message)})"
|
|
1744
|
+
)
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
private fun handleVideoResult(resultCode: Int, data: Intent?) {
|
|
1748
|
+
if (closed) return
|
|
1749
|
+
val uri = data?.data
|
|
1750
|
+
if (resultCode != Activity.RESULT_OK) {
|
|
1751
|
+
evaluatePromiseJavascript("window._craftVideoReject && window._craftVideoReject('Cancelled')")
|
|
1752
|
+
return
|
|
1753
|
+
}
|
|
1754
|
+
if (uri == null) {
|
|
1755
|
+
evaluatePromiseJavascript(
|
|
1756
|
+
"window._craftVideoReject && window._craftVideoReject('Video capture returned no file')"
|
|
1757
|
+
)
|
|
1758
|
+
return
|
|
1468
1759
|
}
|
|
1760
|
+
|
|
1761
|
+
Thread {
|
|
1762
|
+
try {
|
|
1763
|
+
val bytes = activity.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
|
1764
|
+
?: throw IllegalStateException("Recorded video could not be opened")
|
|
1765
|
+
val mimeType = activity.contentResolver.getType(uri) ?: "video/mp4"
|
|
1766
|
+
val encoded = Base64.encodeToString(bytes, Base64.NO_WRAP)
|
|
1767
|
+
val result = "data:$mimeType;base64,$encoded"
|
|
1768
|
+
evaluatePromiseJavascript(
|
|
1769
|
+
"window._craftVideoResolve && window._craftVideoResolve(${jsQuote(result)})"
|
|
1770
|
+
)
|
|
1771
|
+
} catch (e: Exception) {
|
|
1772
|
+
evaluatePromiseJavascript(
|
|
1773
|
+
"window._craftVideoReject && window._craftVideoReject(${jsQuote(e.message ?: "Recorded video could not be read")})"
|
|
1774
|
+
)
|
|
1775
|
+
}
|
|
1776
|
+
}.start()
|
|
1469
1777
|
}
|
|
1470
1778
|
|
|
1471
1779
|
private fun bitmapToBase64(bitmap: Bitmap): String {
|
|
@@ -1484,49 +1792,65 @@ class CraftBridge(
|
|
|
1484
1792
|
|
|
1485
1793
|
@JavascriptInterface
|
|
1486
1794
|
fun authenticate(reason: String) {
|
|
1795
|
+
// The prompt and its Java callback live in CraftNative; every result
|
|
1796
|
+
// comes back through Zig, which settles the existing page promise.
|
|
1797
|
+
if (CraftNative.authenticate(activity, reason)) return
|
|
1798
|
+
|
|
1799
|
+
val settled = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
1800
|
+
|
|
1801
|
+
fun resolveBiometricRequest() {
|
|
1802
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
1803
|
+
biometricPrompt = null
|
|
1804
|
+
evaluatePromiseJavascript("window._craftBiometricResolve && window._craftBiometricResolve(true)")
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
fun rejectBiometricRequest(message: String) {
|
|
1808
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
1809
|
+
biometricPrompt = null
|
|
1810
|
+
evaluatePromiseJavascript(
|
|
1811
|
+
"window._craftBiometricReject && window._craftBiometricReject(${jsQuote(message)})"
|
|
1812
|
+
)
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1487
1815
|
if (activity !is FragmentActivity) {
|
|
1488
|
-
|
|
1489
|
-
webView.evaluateJavascript(
|
|
1490
|
-
"window._craftBiometricReject && window._craftBiometricReject('Activity not supported')",
|
|
1491
|
-
null
|
|
1492
|
-
)
|
|
1493
|
-
}
|
|
1816
|
+
rejectBiometricRequest("Activity not supported")
|
|
1494
1817
|
return
|
|
1495
1818
|
}
|
|
1496
1819
|
|
|
1497
1820
|
activity.runOnUiThread {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
.
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1821
|
+
if (closed) return@runOnUiThread
|
|
1822
|
+
try {
|
|
1823
|
+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
1824
|
+
.setTitle("Authenticate")
|
|
1825
|
+
.setSubtitle(reason)
|
|
1826
|
+
.setNegativeButtonText("Cancel")
|
|
1827
|
+
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
|
1828
|
+
.build()
|
|
1829
|
+
|
|
1830
|
+
runCatching { biometricPrompt?.cancelAuthentication() }
|
|
1831
|
+
val prompt = BiometricPrompt(
|
|
1832
|
+
activity as FragmentActivity,
|
|
1833
|
+
mainExecutor,
|
|
1834
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
1835
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
1836
|
+
resolveBiometricRequest()
|
|
1837
|
+
}
|
|
1515
1838
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
null
|
|
1520
|
-
)
|
|
1521
|
-
}
|
|
1839
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
1840
|
+
rejectBiometricRequest(errString.toString())
|
|
1841
|
+
}
|
|
1522
1842
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1843
|
+
override fun onAuthenticationFailed() {
|
|
1844
|
+
// Don't reject yet - user can retry
|
|
1845
|
+
}
|
|
1525
1846
|
}
|
|
1526
|
-
|
|
1527
|
-
)
|
|
1847
|
+
)
|
|
1528
1848
|
|
|
1529
|
-
|
|
1849
|
+
biometricPrompt = prompt
|
|
1850
|
+
prompt.authenticate(promptInfo)
|
|
1851
|
+
} catch (error: Exception) {
|
|
1852
|
+
rejectBiometricRequest(error.message ?: "Biometric authentication failed")
|
|
1853
|
+
}
|
|
1530
1854
|
}
|
|
1531
1855
|
}
|
|
1532
1856
|
|
|
@@ -1541,6 +1865,8 @@ class CraftBridge(
|
|
|
1541
1865
|
|
|
1542
1866
|
@JavascriptInterface
|
|
1543
1867
|
fun secureSet(key: String, value: String): Boolean {
|
|
1868
|
+
if (CraftNative.secureSet(securePrefs, key, value)) return true
|
|
1869
|
+
|
|
1544
1870
|
return try {
|
|
1545
1871
|
securePrefs.edit().putString(key, value).apply()
|
|
1546
1872
|
true
|
|
@@ -1551,6 +1877,14 @@ class CraftBridge(
|
|
|
1551
1877
|
|
|
1552
1878
|
@JavascriptInterface
|
|
1553
1879
|
fun secureGet(key: String): String? {
|
|
1880
|
+
// Three answers, not two: Declined falls through, NotFound is a real
|
|
1881
|
+
// null, Found is a real value that may be the empty string.
|
|
1882
|
+
when (val read = CraftNative.secureGet(securePrefs, key)) {
|
|
1883
|
+
is CraftNative.SecureRead.Found -> return read.value
|
|
1884
|
+
CraftNative.SecureRead.NotFound -> return null
|
|
1885
|
+
CraftNative.SecureRead.Declined -> Unit
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1554
1888
|
return try {
|
|
1555
1889
|
securePrefs.getString(key, null)
|
|
1556
1890
|
} catch (e: Exception) {
|
|
@@ -1560,6 +1894,8 @@ class CraftBridge(
|
|
|
1560
1894
|
|
|
1561
1895
|
@JavascriptInterface
|
|
1562
1896
|
fun secureRemove(key: String): Boolean {
|
|
1897
|
+
if (CraftNative.secureRemove(securePrefs, key)) return true
|
|
1898
|
+
|
|
1563
1899
|
return try {
|
|
1564
1900
|
securePrefs.edit().remove(key).apply()
|
|
1565
1901
|
true
|
|
@@ -1570,6 +1906,8 @@ class CraftBridge(
|
|
|
1570
1906
|
|
|
1571
1907
|
@JavascriptInterface
|
|
1572
1908
|
fun secureClear(): Boolean {
|
|
1909
|
+
if (CraftNative.secureClear(securePrefs)) return true
|
|
1910
|
+
|
|
1573
1911
|
return try {
|
|
1574
1912
|
securePrefs.edit().clear().apply()
|
|
1575
1913
|
true
|
|
@@ -1582,104 +1920,271 @@ class CraftBridge(
|
|
|
1582
1920
|
|
|
1583
1921
|
@JavascriptInterface
|
|
1584
1922
|
fun log(message: String) {
|
|
1923
|
+
if (CraftNative.log(message)) return
|
|
1585
1924
|
android.util.Log.d("CraftBridge", message)
|
|
1586
1925
|
}
|
|
1587
1926
|
|
|
1588
1927
|
// ==================== Geolocation ====================
|
|
1589
1928
|
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION)
|
|
1593
|
-
!= PackageManager.PERMISSION_GRANTED) {
|
|
1594
|
-
ActivityCompat.requestPermissions(
|
|
1595
|
-
activity,
|
|
1596
|
-
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
|
|
1597
|
-
REQUEST_LOCATION
|
|
1598
|
-
)
|
|
1599
|
-
activity.runOnUiThread {
|
|
1600
|
-
webView.evaluateJavascript(
|
|
1601
|
-
"window._craftLocationReject && window._craftLocationReject({code: 1, message: 'Permission denied'})",
|
|
1602
|
-
null
|
|
1603
|
-
)
|
|
1604
|
-
}
|
|
1605
|
-
return
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
|
|
1609
|
-
fusedLocationClient?.lastLocation?.addOnSuccessListener { location: Location? ->
|
|
1610
|
-
if (location != null) {
|
|
1611
|
-
val json = JSONObject().apply {
|
|
1612
|
-
put("latitude", location.latitude)
|
|
1613
|
-
put("longitude", location.longitude)
|
|
1614
|
-
put("accuracy", location.accuracy)
|
|
1615
|
-
put("altitude", location.altitude)
|
|
1616
|
-
put("speed", location.speed)
|
|
1617
|
-
put("heading", location.bearing)
|
|
1618
|
-
put("timestamp", location.time)
|
|
1619
|
-
}
|
|
1620
|
-
activity.runOnUiThread {
|
|
1621
|
-
webView.evaluateJavascript(
|
|
1622
|
-
"window._craftLocationResolve && window._craftLocationResolve($json)",
|
|
1623
|
-
null
|
|
1624
|
-
)
|
|
1625
|
-
}
|
|
1626
|
-
} else {
|
|
1627
|
-
// Request fresh location if last location is null
|
|
1628
|
-
requestFreshLocation()
|
|
1629
|
-
}
|
|
1630
|
-
}?.addOnFailureListener { e ->
|
|
1631
|
-
activity.runOnUiThread {
|
|
1632
|
-
webView.evaluateJavascript(
|
|
1633
|
-
"window._craftLocationReject && window._craftLocationReject({code: 2, message: '${e.message}'})",
|
|
1634
|
-
null
|
|
1635
|
-
)
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1929
|
+
private fun nativePermissionStatus(name: String): String {
|
|
1930
|
+
return CraftPermissionPolicy.status(name, Build.VERSION.SDK_INT, ::isPermissionGranted)
|
|
1638
1931
|
}
|
|
1639
1932
|
|
|
1640
|
-
private fun
|
|
1641
|
-
|
|
1642
|
-
return
|
|
1643
|
-
}
|
|
1933
|
+
private fun isPermissionGranted(permission: String): Boolean =
|
|
1934
|
+
ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED
|
|
1644
1935
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
.setMinUpdateIntervalMillis(500)
|
|
1648
|
-
.setMaxUpdateDelayMillis(1000)
|
|
1649
|
-
.setMaxUpdates(1)
|
|
1650
|
-
.build()
|
|
1936
|
+
private fun hasForegroundLocationPermission(): Boolean =
|
|
1937
|
+
CraftPermissionPolicy.foregroundLocationIsGranted(::isPermissionGranted)
|
|
1651
1938
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
put("heading", location.bearing)
|
|
1664
|
-
put("timestamp", location.time)
|
|
1665
|
-
}
|
|
1666
|
-
activity.runOnUiThread {
|
|
1667
|
-
webView.evaluateJavascript(
|
|
1668
|
-
"window._craftLocationResolve && window._craftLocationResolve($json)",
|
|
1669
|
-
null
|
|
1670
|
-
)
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1939
|
+
private fun claimPermissionRequestCode(): Int? {
|
|
1940
|
+
var requestCode = nextPermissionRequestCode
|
|
1941
|
+
while (pendingPermissionRequests.containsKey(requestCode)) {
|
|
1942
|
+
requestCode += 1
|
|
1943
|
+
if (requestCode > PERMISSION_REQUEST_END) requestCode = PERMISSION_REQUEST_START
|
|
1944
|
+
if (requestCode == nextPermissionRequestCode) return null
|
|
1945
|
+
}
|
|
1946
|
+
nextPermissionRequestCode = if (requestCode == PERMISSION_REQUEST_END) {
|
|
1947
|
+
PERMISSION_REQUEST_START
|
|
1948
|
+
} else {
|
|
1949
|
+
requestCode + 1
|
|
1674
1950
|
}
|
|
1951
|
+
return requestCode
|
|
1952
|
+
}
|
|
1675
1953
|
|
|
1676
|
-
|
|
1954
|
+
private fun enqueuePermissionRequest(
|
|
1955
|
+
permission: String,
|
|
1956
|
+
callbackId: Int,
|
|
1957
|
+
startingAt: Int
|
|
1958
|
+
): Boolean {
|
|
1959
|
+
val request = CraftPermissionPolicy.nextRequest(
|
|
1960
|
+
permission,
|
|
1961
|
+
Build.VERSION.SDK_INT,
|
|
1962
|
+
startingAt,
|
|
1963
|
+
::isPermissionGranted
|
|
1964
|
+
) ?: return false
|
|
1965
|
+
val requestCode = claimPermissionRequestCode() ?: return false
|
|
1966
|
+
pendingPermissionRequests[requestCode] = PendingPermissionRequest(
|
|
1967
|
+
callbackId,
|
|
1968
|
+
permission,
|
|
1969
|
+
request.groupIndex
|
|
1970
|
+
)
|
|
1971
|
+
return try {
|
|
1972
|
+
ActivityCompat.requestPermissions(activity, request.permissions, requestCode)
|
|
1973
|
+
true
|
|
1974
|
+
} catch (error: Exception) {
|
|
1975
|
+
pendingPermissionRequests.remove(requestCode)
|
|
1976
|
+
false
|
|
1977
|
+
}
|
|
1677
1978
|
}
|
|
1678
1979
|
|
|
1679
1980
|
@JavascriptInterface
|
|
1680
|
-
fun
|
|
1681
|
-
|
|
1682
|
-
|
|
1981
|
+
fun checkPermission(permission: String): String = nativePermissionStatus(permission)
|
|
1982
|
+
|
|
1983
|
+
@JavascriptInterface
|
|
1984
|
+
fun requestPermission(permission: String, callbackId: Int) {
|
|
1985
|
+
activity.runOnUiThread {
|
|
1986
|
+
if (closed) return@runOnUiThread
|
|
1987
|
+
val status = nativePermissionStatus(permission)
|
|
1988
|
+
if (status == "granted" || !enqueuePermissionRequest(permission, callbackId, 0)) {
|
|
1989
|
+
deliverPermissionResult(callbackId, status)
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
@JavascriptInterface
|
|
1995
|
+
fun openPermissionSettings() {
|
|
1996
|
+
activity.runOnUiThread {
|
|
1997
|
+
val intent = Intent(
|
|
1998
|
+
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
|
1999
|
+
Uri.fromParts("package", activity.packageName, null)
|
|
2000
|
+
)
|
|
2001
|
+
activity.startActivity(intent)
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
fun onRequestPermissionsResult(requestCode: Int): Boolean {
|
|
2006
|
+
if (closed) return requestCode in PERMISSION_REQUEST_START..PERMISSION_REQUEST_END
|
|
2007
|
+
val pending = pendingPermissionRequests.remove(requestCode) ?: return false
|
|
2008
|
+
// Re-read package state instead of interpreting the parallel callback
|
|
2009
|
+
// arrays. In particular, an approximate-location choice returns a
|
|
2010
|
+
// denied fine result and a granted coarse result, which is still a
|
|
2011
|
+
// granted `location` permission in Craft's contract.
|
|
2012
|
+
val status = nativePermissionStatus(pending.permission)
|
|
2013
|
+
val completedGroup = CraftPermissionPolicy.groupIsGranted(
|
|
2014
|
+
pending.permission,
|
|
2015
|
+
Build.VERSION.SDK_INT,
|
|
2016
|
+
pending.groupIndex,
|
|
2017
|
+
::isPermissionGranted
|
|
2018
|
+
)
|
|
2019
|
+
if (status != "granted" && completedGroup && enqueuePermissionRequest(
|
|
2020
|
+
pending.permission,
|
|
2021
|
+
pending.callbackId,
|
|
2022
|
+
pending.groupIndex + 1
|
|
2023
|
+
)) return true
|
|
2024
|
+
|
|
2025
|
+
deliverPermissionResult(pending.callbackId, status)
|
|
2026
|
+
return true
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
private fun deliverPermissionResult(callbackId: Int, status: String) {
|
|
2030
|
+
if (closed) return
|
|
2031
|
+
activity.runOnUiThread {
|
|
2032
|
+
if (closed) return@runOnUiThread
|
|
2033
|
+
webView.evaluateJavascript(
|
|
2034
|
+
"window.__craftPermissionResult && window.__craftPermissionResult($callbackId, ${jsQuote(status)})",
|
|
2035
|
+
null
|
|
2036
|
+
)
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
@JavascriptInterface
|
|
2041
|
+
fun getCurrentPosition(optionsJson: String) {
|
|
2042
|
+
// The task listeners and the fresh-location callback are
|
|
2043
|
+
// CraftNative's, because they are Java interfaces. fusedLocationClient
|
|
2044
|
+
// below is independent from the callbacks owned by watchPosition, so
|
|
2045
|
+
// a one-shot lookup cannot overwrite or clear a continuous watch.
|
|
2046
|
+
val requestId = currentPositionSequence.incrementAndGet()
|
|
2047
|
+
oneShotLocationCallbacks.forEach { callback ->
|
|
2048
|
+
runCatching { fusedLocationClient?.removeLocationUpdates(callback) }
|
|
2049
|
+
}
|
|
2050
|
+
oneShotLocationCallbacks.clear()
|
|
2051
|
+
oneShotLocationTimeouts.values.forEach { timeout ->
|
|
2052
|
+
oneShotLocationHandler.removeCallbacks(timeout)
|
|
2053
|
+
}
|
|
2054
|
+
oneShotLocationTimeouts.clear()
|
|
2055
|
+
|
|
2056
|
+
if (CraftNative.getCurrentPosition(activity)) return
|
|
2057
|
+
|
|
2058
|
+
val settled = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
2059
|
+
|
|
2060
|
+
fun resolveLocationRequest(location: Location) {
|
|
2061
|
+
if (requestId != currentPositionSequence.get()) return
|
|
2062
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
2063
|
+
val json = JSONObject().apply {
|
|
2064
|
+
put("latitude", location.latitude)
|
|
2065
|
+
put("longitude", location.longitude)
|
|
2066
|
+
put("accuracy", location.accuracy)
|
|
2067
|
+
put("altitude", location.altitude)
|
|
2068
|
+
put("speed", location.speed)
|
|
2069
|
+
put("heading", location.bearing)
|
|
2070
|
+
put("timestamp", location.time)
|
|
2071
|
+
}
|
|
2072
|
+
evaluatePromiseJavascript(
|
|
2073
|
+
"window._craftLocationResolve && window._craftLocationResolve($json)"
|
|
2074
|
+
)
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
fun rejectLocationRequest(message: String, code: Int = 2) {
|
|
2078
|
+
if (requestId != currentPositionSequence.get()) return
|
|
2079
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
2080
|
+
rejectLocation(message, code)
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if (!hasForegroundLocationPermission()) {
|
|
2084
|
+
rejectLocationRequest("Permission denied", 1)
|
|
2085
|
+
requestPermissionsBestEffort(
|
|
2086
|
+
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
|
|
2087
|
+
REQUEST_LOCATION
|
|
2088
|
+
)
|
|
2089
|
+
return
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity) != ConnectionResult.SUCCESS) {
|
|
2093
|
+
rejectLocationRequest("Google Play Services is unavailable")
|
|
2094
|
+
return
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
try {
|
|
2098
|
+
val client = LocationServices.getFusedLocationProviderClient(activity)
|
|
2099
|
+
fusedLocationClient = client
|
|
2100
|
+
client.lastLocation.addOnSuccessListener { location: Location? ->
|
|
2101
|
+
if (requestId != currentPositionSequence.get()) return@addOnSuccessListener
|
|
2102
|
+
if (location != null) {
|
|
2103
|
+
resolveLocationRequest(location)
|
|
2104
|
+
} else {
|
|
2105
|
+
requestFreshLocation(
|
|
2106
|
+
client,
|
|
2107
|
+
requestId,
|
|
2108
|
+
::resolveLocationRequest,
|
|
2109
|
+
{ message -> rejectLocationRequest(message) }
|
|
2110
|
+
)
|
|
2111
|
+
}
|
|
2112
|
+
}.addOnFailureListener { error -> rejectLocationRequest(error.message ?: "Location lookup failed") }
|
|
2113
|
+
} catch (error: Exception) {
|
|
2114
|
+
rejectLocationRequest(error.message ?: "Location lookup failed")
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
private fun requestFreshLocation(
|
|
2119
|
+
client: FusedLocationProviderClient,
|
|
2120
|
+
requestId: Long,
|
|
2121
|
+
resolve: (Location) -> Unit,
|
|
2122
|
+
reject: (String) -> Unit
|
|
2123
|
+
) {
|
|
2124
|
+
if (!hasForegroundLocationPermission()) {
|
|
2125
|
+
reject("Location permission was revoked")
|
|
2126
|
+
return
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000)
|
|
2130
|
+
.setWaitForAccurateLocation(false)
|
|
2131
|
+
.setMinUpdateIntervalMillis(500)
|
|
2132
|
+
.setMaxUpdateDelayMillis(1000)
|
|
2133
|
+
.setMaxUpdates(1)
|
|
2134
|
+
.build()
|
|
2135
|
+
|
|
2136
|
+
val callback = object : LocationCallback() {
|
|
2137
|
+
override fun onLocationResult(result: LocationResult) {
|
|
2138
|
+
clearOneShotLocationCallback(client, this)
|
|
2139
|
+
if (requestId != currentPositionSequence.get()) return
|
|
2140
|
+
result.lastLocation?.let(resolve)
|
|
2141
|
+
?: reject("Location provider returned no position")
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
oneShotLocationCallbacks.add(callback)
|
|
2146
|
+
val timeout = Runnable {
|
|
2147
|
+
if (!oneShotLocationCallbacks.contains(callback)) return@Runnable
|
|
2148
|
+
clearOneShotLocationCallback(client, callback)
|
|
2149
|
+
reject("Location request timed out; provider returned no position")
|
|
2150
|
+
}
|
|
2151
|
+
oneShotLocationTimeouts[callback] = timeout
|
|
2152
|
+
oneShotLocationHandler.postDelayed(timeout, LOCATION_REQUEST_TIMEOUT_MS)
|
|
2153
|
+
try {
|
|
2154
|
+
client.requestLocationUpdates(locationRequest, callback, Looper.getMainLooper())
|
|
2155
|
+
.addOnFailureListener { error ->
|
|
2156
|
+
clearOneShotLocationCallback(client, callback)
|
|
2157
|
+
reject(error.message ?: "Location request failed")
|
|
2158
|
+
}
|
|
2159
|
+
} catch (error: Exception) {
|
|
2160
|
+
clearOneShotLocationCallback(client, callback)
|
|
2161
|
+
reject(error.message ?: "Location request failed")
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
private fun clearOneShotLocationCallback(
|
|
2166
|
+
client: FusedLocationProviderClient,
|
|
2167
|
+
callback: LocationCallback
|
|
2168
|
+
) {
|
|
2169
|
+
runCatching { client.removeLocationUpdates(callback) }
|
|
2170
|
+
oneShotLocationCallbacks.remove(callback)
|
|
2171
|
+
oneShotLocationTimeouts.remove(callback)?.let { timeout ->
|
|
2172
|
+
oneShotLocationHandler.removeCallbacks(timeout)
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
private fun rejectLocation(message: String, code: Int = 2) {
|
|
2177
|
+
evaluatePromiseJavascript(
|
|
2178
|
+
"window._craftLocationReject && window._craftLocationReject({code: $code, message: ${jsQuote(message)}})"
|
|
2179
|
+
)
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
@JavascriptInterface
|
|
2183
|
+
fun watchPosition(optionsJson: String): Int {
|
|
2184
|
+
if (!hasForegroundLocationPermission()) {
|
|
2185
|
+
return -1
|
|
2186
|
+
}
|
|
2187
|
+
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity) != ConnectionResult.SUCCESS) {
|
|
1683
2188
|
return -1
|
|
1684
2189
|
}
|
|
1685
2190
|
|
|
@@ -1690,7 +2195,7 @@ class CraftBridge(
|
|
|
1690
2195
|
.setMinUpdateIntervalMillis(2000)
|
|
1691
2196
|
.build()
|
|
1692
2197
|
|
|
1693
|
-
|
|
2198
|
+
val callback = object : LocationCallback() {
|
|
1694
2199
|
override fun onLocationResult(result: LocationResult) {
|
|
1695
2200
|
val location = result.lastLocation ?: return
|
|
1696
2201
|
val json = JSONObject().apply {
|
|
@@ -1702,17 +2207,15 @@ class CraftBridge(
|
|
|
1702
2207
|
put("heading", location.bearing)
|
|
1703
2208
|
put("timestamp", location.time)
|
|
1704
2209
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
null
|
|
1709
|
-
)
|
|
1710
|
-
}
|
|
2210
|
+
evaluateJavascriptUnlessClosed(
|
|
2211
|
+
"window._craftLocationWatch_$currentWatchId && window._craftLocationWatch_$currentWatchId($json)"
|
|
2212
|
+
)
|
|
1711
2213
|
}
|
|
1712
2214
|
}
|
|
1713
2215
|
|
|
1714
|
-
if (
|
|
1715
|
-
fusedLocationClient?.requestLocationUpdates(locationRequest,
|
|
2216
|
+
if (hasForegroundLocationPermission()) {
|
|
2217
|
+
fusedLocationClient?.requestLocationUpdates(locationRequest, callback, Looper.getMainLooper())
|
|
2218
|
+
locationCallbacks[currentWatchId] = callback
|
|
1716
2219
|
}
|
|
1717
2220
|
|
|
1718
2221
|
return currentWatchId
|
|
@@ -1720,15 +2223,18 @@ class CraftBridge(
|
|
|
1720
2223
|
|
|
1721
2224
|
@JavascriptInterface
|
|
1722
2225
|
fun clearWatch(watchId: Int) {
|
|
1723
|
-
|
|
2226
|
+
locationCallbacks.remove(watchId)?.let {
|
|
1724
2227
|
fusedLocationClient?.removeLocationUpdates(it)
|
|
1725
2228
|
}
|
|
1726
|
-
locationCallback = null
|
|
1727
2229
|
}
|
|
1728
2230
|
|
|
1729
2231
|
@JavascriptInterface
|
|
1730
2232
|
fun startLocationRecording(optionsJson: String): String {
|
|
1731
|
-
|
|
2233
|
+
// Null is "Zig declined". A permission-denied answer is not null — it
|
|
2234
|
+
// is a state object saying so, and returning it here is the answer.
|
|
2235
|
+
CraftNative.startLocationRecording(activity)?.let { return it }
|
|
2236
|
+
|
|
2237
|
+
if (!hasForegroundLocationPermission()) {
|
|
1732
2238
|
return JSONObject().apply {
|
|
1733
2239
|
put("id", JSONObject.NULL)
|
|
1734
2240
|
put("active", false)
|
|
@@ -1748,12 +2254,16 @@ class CraftBridge(
|
|
|
1748
2254
|
|
|
1749
2255
|
@JavascriptInterface
|
|
1750
2256
|
fun pauseLocationRecording(): String {
|
|
2257
|
+
CraftNative.pauseLocationRecording(activity)?.let { return it }
|
|
2258
|
+
|
|
1751
2259
|
CraftLocationRecordingStore.setPaused(activity, true)
|
|
1752
2260
|
return CraftLocationRecordingStore.state(activity).toString()
|
|
1753
2261
|
}
|
|
1754
2262
|
|
|
1755
2263
|
@JavascriptInterface
|
|
1756
2264
|
fun resumeLocationRecording(): String {
|
|
2265
|
+
CraftNative.resumeLocationRecording(activity)?.let { return it }
|
|
2266
|
+
|
|
1757
2267
|
CraftLocationRecordingStore.setPaused(activity, false)
|
|
1758
2268
|
if (CraftLocationRecordingStore.isActive(activity)) {
|
|
1759
2269
|
ContextCompat.startForegroundService(
|
|
@@ -1766,6 +2276,8 @@ class CraftBridge(
|
|
|
1766
2276
|
|
|
1767
2277
|
@JavascriptInterface
|
|
1768
2278
|
fun stopLocationRecording(): String {
|
|
2279
|
+
CraftNative.stopLocationRecording(activity)?.let { return it }
|
|
2280
|
+
|
|
1769
2281
|
CraftLocationRecordingStore.stop(activity)
|
|
1770
2282
|
val result = CraftLocationRecordingStore.state(activity, includeLocations = true).toString()
|
|
1771
2283
|
activity.stopService(Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_STOP))
|
|
@@ -1773,15 +2285,29 @@ class CraftBridge(
|
|
|
1773
2285
|
}
|
|
1774
2286
|
|
|
1775
2287
|
@JavascriptInterface
|
|
1776
|
-
fun getLocationRecordingState(): String
|
|
2288
|
+
fun getLocationRecordingState(): String {
|
|
2289
|
+
// Null is "Zig declined", not "no recording" — an absent recording is
|
|
2290
|
+
// a state object saying so, and returning it here is the answer.
|
|
2291
|
+
CraftNative.getLocationRecordingState(activity)?.let { return it }
|
|
2292
|
+
|
|
2293
|
+
return CraftLocationRecordingStore.state(activity).toString()
|
|
2294
|
+
}
|
|
1777
2295
|
|
|
1778
2296
|
@JavascriptInterface
|
|
1779
|
-
fun readLocationRecording(): String
|
|
2297
|
+
fun readLocationRecording(): String {
|
|
2298
|
+
CraftNative.readLocationRecording(activity)?.let { return it }
|
|
2299
|
+
|
|
2300
|
+
return CraftLocationRecordingStore.locations(activity).toString()
|
|
2301
|
+
}
|
|
1780
2302
|
|
|
1781
2303
|
// ==================== Clipboard ====================
|
|
1782
2304
|
|
|
1783
2305
|
@JavascriptInterface
|
|
1784
2306
|
fun clipboardRead(): String {
|
|
2307
|
+
// Null is "Zig declined", not "empty" — an empty clipboard comes back
|
|
2308
|
+
// as "" and returns here rather than falling through to be read twice.
|
|
2309
|
+
CraftNative.clipboardRead(activity)?.let { return it }
|
|
2310
|
+
|
|
1785
2311
|
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
1786
2312
|
val clip = clipboard.primaryClip
|
|
1787
2313
|
return if (clip != null && clip.itemCount > 0) {
|
|
@@ -1793,6 +2319,8 @@ class CraftBridge(
|
|
|
1793
2319
|
|
|
1794
2320
|
@JavascriptInterface
|
|
1795
2321
|
fun clipboardWrite(text: String): Boolean {
|
|
2322
|
+
if (CraftNative.clipboardWrite(activity, text)) return true
|
|
2323
|
+
|
|
1796
2324
|
return try {
|
|
1797
2325
|
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
1798
2326
|
val clip = ClipData.newPlainText("Craft", text)
|
|
@@ -1807,6 +2335,11 @@ class CraftBridge(
|
|
|
1807
2335
|
|
|
1808
2336
|
@JavascriptInterface
|
|
1809
2337
|
fun getDeviceInfo(): String {
|
|
2338
|
+
// The Zig runtime first, the body below when it declines. Null means
|
|
2339
|
+
// this build does not serve the action natively — a shim-only app, or
|
|
2340
|
+
// one whose libcraft.so failed to bind — and is not an error.
|
|
2341
|
+
CraftNative.getDeviceInfo(activity)?.let { return it }
|
|
2342
|
+
|
|
1810
2343
|
val metrics = DisplayMetrics()
|
|
1811
2344
|
@Suppress("DEPRECATION")
|
|
1812
2345
|
activity.windowManager.defaultDisplay.getMetrics(metrics)
|
|
@@ -1837,32 +2370,12 @@ class CraftBridge(
|
|
|
1837
2370
|
}.toString()
|
|
1838
2371
|
}
|
|
1839
2372
|
|
|
1840
|
-
// ==================== App Badge ====================
|
|
1841
|
-
|
|
1842
|
-
@JavascriptInterface
|
|
1843
|
-
fun setBadge(count: Int) {
|
|
1844
|
-
// Android doesn't have native badge support, but we can use notification channels
|
|
1845
|
-
// This is a simplified implementation - for full support, use ShortcutBadger library
|
|
1846
|
-
try {
|
|
1847
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1848
|
-
val notificationManager = NotificationManagerCompat.from(activity)
|
|
1849
|
-
// Badge is typically shown via notification count
|
|
1850
|
-
// Full implementation would require creating a notification
|
|
1851
|
-
}
|
|
1852
|
-
} catch (e: Exception) {
|
|
1853
|
-
log("Badge not supported: ${e.message}")
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
@JavascriptInterface
|
|
1858
|
-
fun clearBadge() {
|
|
1859
|
-
setBadge(0)
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
2373
|
// ==================== Network Status ====================
|
|
1863
2374
|
|
|
1864
2375
|
@JavascriptInterface
|
|
1865
2376
|
fun getNetworkStatus(): String {
|
|
2377
|
+
CraftNative.getNetworkStatus(activity)?.let { return it }
|
|
2378
|
+
|
|
1866
2379
|
val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
1867
2380
|
val network = connectivityManager.activeNetwork
|
|
1868
2381
|
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
|
@@ -1885,9 +2398,16 @@ class CraftBridge(
|
|
|
1885
2398
|
|
|
1886
2399
|
@JavascriptInterface
|
|
1887
2400
|
fun startNetworkMonitoring() {
|
|
2401
|
+
// The callback object is CraftNative's, because JNI cannot subclass
|
|
2402
|
+
// an abstract Java class — what Zig owns is what happens when it
|
|
2403
|
+
// fires. Returning here keeps this file's networkCallback null, which
|
|
2404
|
+
// is why stopNetworkMonitoring has to reach the same way.
|
|
2405
|
+
if (CraftNative.startNetworkMonitoring(activity)) return
|
|
2406
|
+
if (networkCallback != null) return
|
|
2407
|
+
|
|
1888
2408
|
val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
1889
2409
|
|
|
1890
|
-
|
|
2410
|
+
val callback = object : ConnectivityManager.NetworkCallback() {
|
|
1891
2411
|
override fun onAvailable(network: Network) {
|
|
1892
2412
|
sendNetworkChange()
|
|
1893
2413
|
}
|
|
@@ -1905,11 +2425,14 @@ class CraftBridge(
|
|
|
1905
2425
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
1906
2426
|
.build()
|
|
1907
2427
|
|
|
1908
|
-
connectivityManager.registerNetworkCallback(request,
|
|
2428
|
+
connectivityManager.registerNetworkCallback(request, callback)
|
|
2429
|
+
networkCallback = callback
|
|
1909
2430
|
}
|
|
1910
2431
|
|
|
1911
2432
|
@JavascriptInterface
|
|
1912
2433
|
fun stopNetworkMonitoring() {
|
|
2434
|
+
if (CraftNative.stopNetworkMonitoring(activity)) return
|
|
2435
|
+
|
|
1913
2436
|
val connectivityManager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
1914
2437
|
networkCallback?.let {
|
|
1915
2438
|
connectivityManager.unregisterNetworkCallback(it)
|
|
@@ -1919,42 +2442,49 @@ class CraftBridge(
|
|
|
1919
2442
|
|
|
1920
2443
|
private fun sendNetworkChange() {
|
|
1921
2444
|
val status = getNetworkStatus()
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
null
|
|
1926
|
-
)
|
|
1927
|
-
}
|
|
2445
|
+
evaluateJavascriptUnlessClosed(
|
|
2446
|
+
"window._craftNetworkChangeCallback && window._craftNetworkChangeCallback($status)"
|
|
2447
|
+
)
|
|
1928
2448
|
}
|
|
1929
2449
|
|
|
1930
2450
|
// ==================== App Review ====================
|
|
1931
2451
|
|
|
1932
2452
|
@JavascriptInterface
|
|
1933
2453
|
fun requestReview() {
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2454
|
+
if (CraftNative.requestReview(activity)) return
|
|
2455
|
+
|
|
2456
|
+
val settled = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
2457
|
+
|
|
2458
|
+
fun resolveReviewRequest() {
|
|
2459
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
2460
|
+
evaluatePromiseJavascript("window._craftReviewResolve && window._craftReviewResolve(true)")
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
fun rejectReviewRequest(message: String) {
|
|
2464
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
2465
|
+
evaluatePromiseJavascript(
|
|
2466
|
+
"window._craftReviewReject && window._craftReviewReject(${jsQuote(message)})"
|
|
2467
|
+
)
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
try {
|
|
2471
|
+
val reviewManager = ReviewManagerFactory.create(activity)
|
|
2472
|
+
val request = reviewManager.requestReviewFlow()
|
|
2473
|
+
|
|
2474
|
+
request.addOnCompleteListener { task ->
|
|
2475
|
+
if (task.isSuccessful) {
|
|
2476
|
+
try {
|
|
2477
|
+
reviewManager.launchReviewFlow(activity, task.result)
|
|
2478
|
+
.addOnCompleteListener { resolveReviewRequest() }
|
|
2479
|
+
} catch (error: Exception) {
|
|
2480
|
+
rejectReviewRequest(error.message ?: "Review flow failed")
|
|
1947
2481
|
}
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
activity.runOnUiThread {
|
|
1951
|
-
val message = JSONObject.quote(task.exception?.message ?: "Review flow failed")
|
|
1952
|
-
webView.evaluateJavascript(
|
|
1953
|
-
"window._craftReviewReject && window._craftReviewReject($message)",
|
|
1954
|
-
null
|
|
1955
|
-
)
|
|
2482
|
+
} else {
|
|
2483
|
+
rejectReviewRequest(task.exception?.message ?: "Review flow failed")
|
|
1956
2484
|
}
|
|
1957
2485
|
}
|
|
2486
|
+
} catch (error: Exception) {
|
|
2487
|
+
rejectReviewRequest(error.message ?: "Review flow failed")
|
|
1958
2488
|
}
|
|
1959
2489
|
}
|
|
1960
2490
|
|
|
@@ -1985,6 +2515,8 @@ class CraftBridge(
|
|
|
1985
2515
|
}
|
|
1986
2516
|
} catch (e: CameraAccessException) {
|
|
1987
2517
|
false
|
|
2518
|
+
} catch (e: SecurityException) {
|
|
2519
|
+
false
|
|
1988
2520
|
}
|
|
1989
2521
|
}
|
|
1990
2522
|
|
|
@@ -1998,6 +2530,8 @@ class CraftBridge(
|
|
|
1998
2530
|
|
|
1999
2531
|
@JavascriptInterface
|
|
2000
2532
|
fun openURL(url: String): Boolean {
|
|
2533
|
+
if (CraftNative.openURL(activity, url)) return true
|
|
2534
|
+
|
|
2001
2535
|
return try {
|
|
2002
2536
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
2003
2537
|
activity.startActivity(intent)
|
|
@@ -2011,6 +2545,8 @@ class CraftBridge(
|
|
|
2011
2545
|
|
|
2012
2546
|
@JavascriptInterface
|
|
2013
2547
|
fun vibrate(patternJson: String) {
|
|
2548
|
+
if (CraftNative.vibrate(activity, patternJson)) return
|
|
2549
|
+
|
|
2014
2550
|
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
2015
2551
|
val vibratorManager = activity.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
|
2016
2552
|
vibratorManager.defaultVibrator
|
|
@@ -2039,11 +2575,21 @@ class CraftBridge(
|
|
|
2039
2575
|
|
|
2040
2576
|
@JavascriptInterface
|
|
2041
2577
|
fun getAppState(): String {
|
|
2578
|
+
// Null is "Zig declined". Otherwise this is the state Zig's observer
|
|
2579
|
+
// has been maintaining, and currentAppState below is the one this
|
|
2580
|
+
// file's observer maintains — whichever of the two is running.
|
|
2581
|
+
CraftNative.getAppState()?.let { return it }
|
|
2582
|
+
|
|
2042
2583
|
return currentAppState
|
|
2043
2584
|
}
|
|
2044
2585
|
|
|
2045
2586
|
@JavascriptInterface
|
|
2046
2587
|
fun startAppStateMonitoring() {
|
|
2588
|
+
// The observer object is CraftNative's, and so is the state it
|
|
2589
|
+
// maintains — which is why getAppState below has to reach the same
|
|
2590
|
+
// way. All three move together or none of them can.
|
|
2591
|
+
if (CraftNative.startAppStateMonitoring(activity)) return
|
|
2592
|
+
|
|
2047
2593
|
activity.runOnUiThread {
|
|
2048
2594
|
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateObserver)
|
|
2049
2595
|
}
|
|
@@ -2051,6 +2597,8 @@ class CraftBridge(
|
|
|
2051
2597
|
|
|
2052
2598
|
@JavascriptInterface
|
|
2053
2599
|
fun stopAppStateMonitoring() {
|
|
2600
|
+
if (CraftNative.stopAppStateMonitoring(activity)) return
|
|
2601
|
+
|
|
2054
2602
|
activity.runOnUiThread {
|
|
2055
2603
|
ProcessLifecycleOwner.get().lifecycle.removeObserver(appStateObserver)
|
|
2056
2604
|
}
|
|
@@ -2066,12 +2614,9 @@ class CraftBridge(
|
|
|
2066
2614
|
|
|
2067
2615
|
if (newState != currentAppState) {
|
|
2068
2616
|
currentAppState = newState
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
null
|
|
2073
|
-
)
|
|
2074
|
-
}
|
|
2617
|
+
evaluateJavascriptUnlessClosed(
|
|
2618
|
+
"window._craftAppStateCallback && window._craftAppStateCallback(${jsQuote(currentAppState)})"
|
|
2619
|
+
)
|
|
2075
2620
|
sendEvent("craftAppStateChange", mapOf("state" to currentAppState))
|
|
2076
2621
|
}
|
|
2077
2622
|
}
|
|
@@ -2080,83 +2625,99 @@ class CraftBridge(
|
|
|
2080
2625
|
|
|
2081
2626
|
@JavascriptInterface
|
|
2082
2627
|
fun getContacts() {
|
|
2628
|
+
// Zig answers through the reply channel, so returning here is what
|
|
2629
|
+
// stops the promise being settled twice.
|
|
2630
|
+
if (CraftNative.getContacts(activity)) return
|
|
2631
|
+
|
|
2083
2632
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS)
|
|
2084
2633
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2634
|
+
evaluatePromiseJavascript("window._craftContactsReject && window._craftContactsReject('Permission denied')")
|
|
2635
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.READ_CONTACTS), REQUEST_CONTACTS)
|
|
2636
|
+
return
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
val contacts = try {
|
|
2640
|
+
loadContacts()
|
|
2641
|
+
} catch (error: Exception) {
|
|
2642
|
+
evaluatePromiseJavascript(
|
|
2643
|
+
"window._craftContactsReject && window._craftContactsReject(${jsQuote(error.message ?: "Contacts could not be read")})"
|
|
2644
|
+
)
|
|
2089
2645
|
return
|
|
2090
2646
|
}
|
|
2091
2647
|
|
|
2092
|
-
|
|
2648
|
+
evaluatePromiseJavascript("window._craftContactsResolve && window._craftContactsResolve($contacts)")
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
private fun loadContacts(): JSONArray {
|
|
2652
|
+
val contactRows = mutableListOf<Pair<String?, String?>>()
|
|
2093
2653
|
val cursor: Cursor? = activity.contentResolver.query(
|
|
2094
2654
|
ContactsContract.Contacts.CONTENT_URI,
|
|
2095
|
-
|
|
2655
|
+
arrayOf(
|
|
2656
|
+
ContactsContract.Contacts._ID,
|
|
2657
|
+
ContactsContract.Contacts.DISPLAY_NAME
|
|
2658
|
+
),
|
|
2659
|
+
null, null, ContactsContract.Contacts.DISPLAY_NAME + " ASC"
|
|
2096
2660
|
)
|
|
2097
2661
|
|
|
2098
2662
|
cursor?.use {
|
|
2663
|
+
val idIndex = it.getColumnIndexOrThrow(ContactsContract.Contacts._ID)
|
|
2664
|
+
val nameIndex = it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)
|
|
2099
2665
|
while (it.moveToNext()) {
|
|
2100
|
-
|
|
2101
|
-
val name = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
|
|
2102
|
-
|
|
2103
|
-
val contact = JSONObject().apply {
|
|
2104
|
-
put("id", id)
|
|
2105
|
-
put("displayName", name ?: "")
|
|
2106
|
-
put("phoneNumbers", getContactPhones(id))
|
|
2107
|
-
put("emailAddresses", getContactEmails(id))
|
|
2108
|
-
}
|
|
2109
|
-
contacts.put(contact)
|
|
2666
|
+
contactRows.add(it.getString(idIndex) to it.getString(nameIndex))
|
|
2110
2667
|
}
|
|
2111
2668
|
}
|
|
2112
2669
|
|
|
2113
|
-
|
|
2114
|
-
webView.evaluateJavascript("window._craftContactsResolve && window._craftContactsResolve($contacts)", null)
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
private fun getContactPhones(contactId: String): JSONArray {
|
|
2119
|
-
val phones = JSONArray()
|
|
2120
|
-
val cursor = activity.contentResolver.query(
|
|
2670
|
+
val phoneNumbers = getContactValues(
|
|
2121
2671
|
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
|
2122
|
-
|
|
2123
|
-
ContactsContract.CommonDataKinds.Phone.
|
|
2124
|
-
arrayOf(contactId), null
|
|
2672
|
+
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
|
|
2673
|
+
ContactsContract.CommonDataKinds.Phone.NUMBER
|
|
2125
2674
|
)
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2675
|
+
val emailAddresses = getContactValues(
|
|
2676
|
+
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
|
|
2677
|
+
ContactsContract.CommonDataKinds.Email.CONTACT_ID,
|
|
2678
|
+
ContactsContract.CommonDataKinds.Email.ADDRESS
|
|
2679
|
+
)
|
|
2680
|
+
|
|
2681
|
+
return JSONArray().apply {
|
|
2682
|
+
contactRows.forEach { (id, name) ->
|
|
2683
|
+
put(JSONObject().apply {
|
|
2684
|
+
put("id", id)
|
|
2685
|
+
put("displayName", name ?: "")
|
|
2686
|
+
put("phoneNumbers", id?.let { phoneNumbers[it] } ?: JSONArray())
|
|
2687
|
+
put("emailAddresses", id?.let { emailAddresses[it] } ?: JSONArray())
|
|
2688
|
+
})
|
|
2130
2689
|
}
|
|
2131
2690
|
}
|
|
2132
|
-
return phones
|
|
2133
2691
|
}
|
|
2134
2692
|
|
|
2135
|
-
private fun
|
|
2136
|
-
val
|
|
2693
|
+
private fun getContactValues(contentUri: Uri, contactIdColumn: String, valueColumn: String): Map<String, JSONArray> {
|
|
2694
|
+
val values = mutableMapOf<String, JSONArray>()
|
|
2137
2695
|
val cursor = activity.contentResolver.query(
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
arrayOf(contactId), null
|
|
2696
|
+
contentUri,
|
|
2697
|
+
arrayOf(contactIdColumn, valueColumn),
|
|
2698
|
+
null, null, null
|
|
2142
2699
|
)
|
|
2143
2700
|
cursor?.use {
|
|
2701
|
+
val contactIdIndex = it.getColumnIndexOrThrow(contactIdColumn)
|
|
2702
|
+
val valueIndex = it.getColumnIndexOrThrow(valueColumn)
|
|
2144
2703
|
while (it.moveToNext()) {
|
|
2145
|
-
val
|
|
2146
|
-
|
|
2704
|
+
val contactId = it.getString(contactIdIndex) ?: continue
|
|
2705
|
+
values.getOrPut(contactId) { JSONArray() }.put(it.getString(valueIndex))
|
|
2147
2706
|
}
|
|
2148
2707
|
}
|
|
2149
|
-
return
|
|
2708
|
+
return values
|
|
2150
2709
|
}
|
|
2151
2710
|
|
|
2152
2711
|
@JavascriptInterface
|
|
2153
2712
|
fun addContact(contactJson: String) {
|
|
2713
|
+
// Zig answers through the reply channel, so returning here is what
|
|
2714
|
+
// stops the promise being settled twice.
|
|
2715
|
+
if (CraftNative.addContact(activity, contactJson)) return
|
|
2716
|
+
|
|
2154
2717
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CONTACTS)
|
|
2155
2718
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
webView.evaluateJavascript("window._craftAddContactReject && window._craftAddContactReject('Permission denied')", null)
|
|
2159
|
-
}
|
|
2719
|
+
evaluatePromiseJavascript("window._craftAddContactReject && window._craftAddContactReject('Permission denied')")
|
|
2720
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.WRITE_CONTACTS), REQUEST_CONTACTS)
|
|
2160
2721
|
return
|
|
2161
2722
|
}
|
|
2162
2723
|
|
|
@@ -2204,13 +2765,13 @@ class CraftBridge(
|
|
|
2204
2765
|
val results = activity.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
|
2205
2766
|
val contactId = ContentUris.parseId(results[0].uri!!)
|
|
2206
2767
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2768
|
+
evaluatePromiseJavascript(
|
|
2769
|
+
"window._craftAddContactResolve && window._craftAddContactResolve(${jsQuote(contactId)})"
|
|
2770
|
+
)
|
|
2210
2771
|
} catch (e: Exception) {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2772
|
+
evaluatePromiseJavascript(
|
|
2773
|
+
"window._craftAddContactReject && window._craftAddContactReject(${jsQuote(e.message ?: "Contact could not be added")})"
|
|
2774
|
+
)
|
|
2214
2775
|
}
|
|
2215
2776
|
}
|
|
2216
2777
|
|
|
@@ -2218,65 +2779,73 @@ class CraftBridge(
|
|
|
2218
2779
|
|
|
2219
2780
|
@JavascriptInterface
|
|
2220
2781
|
fun getCalendarEvents(startDateMs: Long, endDateMs: Long) {
|
|
2782
|
+
// Zig answers through the reply channel, so returning here is what
|
|
2783
|
+
// stops the promise being settled twice.
|
|
2784
|
+
if (CraftNative.getCalendarEvents(activity, startDateMs, endDateMs)) return
|
|
2785
|
+
|
|
2221
2786
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CALENDAR)
|
|
2222
2787
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
webView.evaluateJavascript("window._craftCalendarReject && window._craftCalendarReject('Permission denied')", null)
|
|
2226
|
-
}
|
|
2788
|
+
evaluatePromiseJavascript("window._craftCalendarReject && window._craftCalendarReject('Permission denied')")
|
|
2789
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.READ_CALENDAR), REQUEST_CALENDAR)
|
|
2227
2790
|
return
|
|
2228
2791
|
}
|
|
2229
2792
|
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2793
|
+
try {
|
|
2794
|
+
val events = JSONArray()
|
|
2795
|
+
val startTime = if (startDateMs > 0) startDateMs else System.currentTimeMillis()
|
|
2796
|
+
val endTime = if (endDateMs > 0) endDateMs else startTime + (30L * 24 * 60 * 60 * 1000) // 30 days
|
|
2233
2797
|
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2798
|
+
val projection = arrayOf(
|
|
2799
|
+
CalendarContract.Events._ID,
|
|
2800
|
+
CalendarContract.Events.TITLE,
|
|
2801
|
+
CalendarContract.Events.DESCRIPTION,
|
|
2802
|
+
CalendarContract.Events.DTSTART,
|
|
2803
|
+
CalendarContract.Events.DTEND,
|
|
2804
|
+
CalendarContract.Events.EVENT_LOCATION,
|
|
2805
|
+
CalendarContract.Events.ALL_DAY
|
|
2806
|
+
)
|
|
2243
2807
|
|
|
2244
|
-
|
|
2245
|
-
|
|
2808
|
+
val selection = "(${CalendarContract.Events.DTSTART} >= ?) AND (${CalendarContract.Events.DTSTART} <= ?)"
|
|
2809
|
+
val selectionArgs = arrayOf(startTime.toString(), endTime.toString())
|
|
2246
2810
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2811
|
+
val cursor = activity.contentResolver.query(
|
|
2812
|
+
CalendarContract.Events.CONTENT_URI,
|
|
2813
|
+
projection, selection, selectionArgs, CalendarContract.Events.DTSTART + " ASC"
|
|
2814
|
+
)
|
|
2251
2815
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2816
|
+
cursor?.use {
|
|
2817
|
+
while (it.moveToNext()) {
|
|
2818
|
+
val event = JSONObject().apply {
|
|
2819
|
+
put("id", it.getString(0))
|
|
2820
|
+
put("title", it.getString(1) ?: "")
|
|
2821
|
+
put("notes", it.getString(2) ?: "")
|
|
2822
|
+
put("startDate", it.getLong(3))
|
|
2823
|
+
put("endDate", it.getLong(4))
|
|
2824
|
+
put("location", it.getString(5) ?: "")
|
|
2825
|
+
put("isAllDay", it.getInt(6) == 1)
|
|
2826
|
+
}
|
|
2827
|
+
events.put(event)
|
|
2262
2828
|
}
|
|
2263
|
-
events.put(event)
|
|
2264
2829
|
}
|
|
2265
|
-
}
|
|
2266
2830
|
|
|
2267
|
-
|
|
2268
|
-
|
|
2831
|
+
evaluatePromiseJavascript("window._craftCalendarResolve && window._craftCalendarResolve($events)")
|
|
2832
|
+
} catch (e: Exception) {
|
|
2833
|
+
evaluatePromiseJavascript(
|
|
2834
|
+
"window._craftCalendarReject && window._craftCalendarReject(${jsQuote(e.message ?: "Calendar events could not be read")})"
|
|
2835
|
+
)
|
|
2269
2836
|
}
|
|
2270
2837
|
}
|
|
2271
2838
|
|
|
2272
2839
|
@JavascriptInterface
|
|
2273
2840
|
fun createCalendarEvent(eventJson: String) {
|
|
2841
|
+
// Zig answers through the reply channel, so returning here is what
|
|
2842
|
+
// stops the promise being settled twice.
|
|
2843
|
+
if (CraftNative.createCalendarEvent(activity, eventJson)) return
|
|
2844
|
+
|
|
2274
2845
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CALENDAR)
|
|
2275
2846
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
webView.evaluateJavascript("window._craftCreateEventReject && window._craftCreateEventReject('Permission denied')", null)
|
|
2279
|
-
}
|
|
2847
|
+
evaluatePromiseJavascript("window._craftCreateEventReject && window._craftCreateEventReject('Permission denied')")
|
|
2848
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.WRITE_CALENDAR), REQUEST_CALENDAR)
|
|
2280
2849
|
return
|
|
2281
2850
|
}
|
|
2282
2851
|
|
|
@@ -2296,28 +2865,30 @@ class CraftBridge(
|
|
|
2296
2865
|
val uri = activity.contentResolver.insert(CalendarContract.Events.CONTENT_URI, values)
|
|
2297
2866
|
val eventId = uri?.lastPathSegment ?: ""
|
|
2298
2867
|
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2868
|
+
evaluatePromiseJavascript(
|
|
2869
|
+
"window._craftCreateEventResolve && window._craftCreateEventResolve(${jsQuote(eventId)})"
|
|
2870
|
+
)
|
|
2302
2871
|
} catch (e: Exception) {
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2872
|
+
evaluatePromiseJavascript(
|
|
2873
|
+
"window._craftCreateEventReject && window._craftCreateEventReject(${jsQuote(e.message ?: "Calendar event could not be created")})"
|
|
2874
|
+
)
|
|
2306
2875
|
}
|
|
2307
2876
|
}
|
|
2308
2877
|
|
|
2309
2878
|
@JavascriptInterface
|
|
2310
2879
|
fun deleteCalendarEvent(eventId: String) {
|
|
2880
|
+
// Zig answers through the reply channel, so returning here is what
|
|
2881
|
+
// stops the promise being settled twice.
|
|
2882
|
+
if (CraftNative.deleteCalendarEvent(activity, eventId)) return
|
|
2883
|
+
|
|
2311
2884
|
try {
|
|
2312
2885
|
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId.toLong())
|
|
2313
2886
|
activity.contentResolver.delete(uri, null, null)
|
|
2314
|
-
|
|
2315
|
-
webView.evaluateJavascript("window._craftDeleteEventResolve && window._craftDeleteEventResolve(true)", null)
|
|
2316
|
-
}
|
|
2887
|
+
evaluatePromiseJavascript("window._craftDeleteEventResolve && window._craftDeleteEventResolve(true)")
|
|
2317
2888
|
} catch (e: Exception) {
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2889
|
+
evaluatePromiseJavascript(
|
|
2890
|
+
"window._craftDeleteEventReject && window._craftDeleteEventReject(${jsQuote(e.message ?: "Calendar event could not be deleted")})"
|
|
2891
|
+
)
|
|
2321
2892
|
}
|
|
2322
2893
|
}
|
|
2323
2894
|
|
|
@@ -2325,6 +2896,11 @@ class CraftBridge(
|
|
|
2325
2896
|
|
|
2326
2897
|
@JavascriptInterface
|
|
2327
2898
|
fun scheduleNotification(notificationJson: String) {
|
|
2899
|
+
// Zig answers through the reply channel, so returning here is what
|
|
2900
|
+
// stops the promise being settled twice. It declines a delayed
|
|
2901
|
+
// notification before doing any of the work below.
|
|
2902
|
+
if (CraftNative.scheduleNotification(activity, notificationJson)) return
|
|
2903
|
+
|
|
2328
2904
|
try {
|
|
2329
2905
|
val notif = JSONObject(notificationJson)
|
|
2330
2906
|
val title = notif.optString("title", "")
|
|
@@ -2352,13 +2928,13 @@ class CraftBridge(
|
|
|
2352
2928
|
notificationManager.notify(id.hashCode(), builder.build())
|
|
2353
2929
|
}
|
|
2354
2930
|
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2931
|
+
evaluatePromiseJavascript(
|
|
2932
|
+
"window._craftNotifResolve && window._craftNotifResolve(${jsQuote(id)})"
|
|
2933
|
+
)
|
|
2358
2934
|
} catch (e: Exception) {
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2935
|
+
evaluatePromiseJavascript(
|
|
2936
|
+
"window._craftNotifReject && window._craftNotifReject(${jsQuote(e.message ?: "Notification could not be scheduled")})"
|
|
2937
|
+
)
|
|
2362
2938
|
}
|
|
2363
2939
|
}
|
|
2364
2940
|
|
|
@@ -2377,12 +2953,16 @@ class CraftBridge(
|
|
|
2377
2953
|
|
|
2378
2954
|
@JavascriptInterface
|
|
2379
2955
|
fun cancelNotification(id: String) {
|
|
2956
|
+
if (CraftNative.cancelNotification(activity, id)) return
|
|
2957
|
+
|
|
2380
2958
|
val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
2381
2959
|
notificationManager.cancel(id.hashCode())
|
|
2382
2960
|
}
|
|
2383
2961
|
|
|
2384
2962
|
@JavascriptInterface
|
|
2385
2963
|
fun cancelAllNotifications() {
|
|
2964
|
+
if (CraftNative.cancelAllNotifications(activity)) return
|
|
2965
|
+
|
|
2386
2966
|
val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
2387
2967
|
notificationManager.cancelAll()
|
|
2388
2968
|
}
|
|
@@ -2391,6 +2971,20 @@ class CraftBridge(
|
|
|
2391
2971
|
|
|
2392
2972
|
@JavascriptInterface
|
|
2393
2973
|
fun getProducts(productIdsJson: String) {
|
|
2974
|
+
if (CraftNative.getProducts(activity, productIdsJson)) return
|
|
2975
|
+
|
|
2976
|
+
val settled = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
2977
|
+
|
|
2978
|
+
fun resolveProductRequest(products: JSONArray) {
|
|
2979
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
2980
|
+
evaluatePromiseJavascript("window._craftProductsResolve && window._craftProductsResolve($products)")
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
fun rejectProductRequest(message: String) {
|
|
2984
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
2985
|
+
rejectProducts(message)
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2394
2988
|
try {
|
|
2395
2989
|
val productIds = JSONArray(productIdsJson)
|
|
2396
2990
|
val productList = mutableListOf<String>()
|
|
@@ -2398,12 +2992,14 @@ class CraftBridge(
|
|
|
2398
2992
|
productList.add(productIds.getString(i))
|
|
2399
2993
|
}
|
|
2400
2994
|
|
|
2401
|
-
|
|
2995
|
+
runCatching { productBillingClient?.endConnection() }
|
|
2996
|
+
val client = BillingClient.newBuilder(activity)
|
|
2402
2997
|
.setListener { _, _ -> }
|
|
2403
2998
|
.enablePendingPurchases()
|
|
2404
2999
|
.build()
|
|
3000
|
+
productBillingClient = client
|
|
2405
3001
|
|
|
2406
|
-
|
|
3002
|
+
client.startConnection(object : BillingClientStateListener {
|
|
2407
3003
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
2408
3004
|
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
2409
3005
|
val params = QueryProductDetailsParams.newBuilder()
|
|
@@ -2415,96 +3011,166 @@ class CraftBridge(
|
|
|
2415
3011
|
})
|
|
2416
3012
|
.build()
|
|
2417
3013
|
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
put(
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('Query failed')", null)
|
|
3014
|
+
try {
|
|
3015
|
+
client.queryProductDetailsAsync(params) { result, productDetailsList ->
|
|
3016
|
+
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
3017
|
+
val products = JSONArray()
|
|
3018
|
+
productDetailsList.forEach { details ->
|
|
3019
|
+
products.put(JSONObject().apply {
|
|
3020
|
+
put("id", details.productId)
|
|
3021
|
+
put("title", details.name)
|
|
3022
|
+
put("displayName", details.name)
|
|
3023
|
+
put("description", details.description)
|
|
3024
|
+
put("price", details.oneTimePurchaseOfferDetails?.formattedPrice ?: "")
|
|
3025
|
+
})
|
|
3026
|
+
}
|
|
3027
|
+
resolveProductRequest(products)
|
|
3028
|
+
} else {
|
|
3029
|
+
rejectProductRequest("Product query failed: ${result.debugMessage}")
|
|
2435
3030
|
}
|
|
2436
3031
|
}
|
|
3032
|
+
} catch (error: Exception) {
|
|
3033
|
+
rejectProductRequest(error.message ?: "Product query failed")
|
|
2437
3034
|
}
|
|
3035
|
+
} else {
|
|
3036
|
+
rejectProductRequest("Billing setup failed: ${billingResult.debugMessage}")
|
|
2438
3037
|
}
|
|
2439
3038
|
}
|
|
2440
3039
|
|
|
2441
3040
|
override fun onBillingServiceDisconnected() {
|
|
2442
|
-
|
|
2443
|
-
webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('Billing disconnected')", null)
|
|
2444
|
-
}
|
|
3041
|
+
rejectProductRequest("Billing disconnected")
|
|
2445
3042
|
}
|
|
2446
3043
|
})
|
|
2447
3044
|
} catch (e: Exception) {
|
|
2448
|
-
|
|
2449
|
-
webView.evaluateJavascript("window._craftProductsReject && window._craftProductsReject('${e.message}')", null)
|
|
2450
|
-
}
|
|
3045
|
+
rejectProductRequest(e.message ?: "Product query failed")
|
|
2451
3046
|
}
|
|
2452
3047
|
}
|
|
2453
3048
|
|
|
3049
|
+
private fun rejectProducts(message: String) {
|
|
3050
|
+
evaluatePromiseJavascript(
|
|
3051
|
+
"window._craftProductsReject && window._craftProductsReject(${jsQuote(message)})"
|
|
3052
|
+
)
|
|
3053
|
+
}
|
|
3054
|
+
|
|
2454
3055
|
@JavascriptInterface
|
|
2455
3056
|
fun purchase(productId: String) {
|
|
2456
3057
|
// Simplified - real implementation would query product details first
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
3058
|
+
evaluatePromiseJavascript(
|
|
3059
|
+
"window._craftPurchaseReject && window._craftPurchaseReject('Purchase flow not fully implemented')"
|
|
3060
|
+
)
|
|
2460
3061
|
}
|
|
2461
3062
|
|
|
2462
3063
|
@JavascriptInterface
|
|
2463
3064
|
fun restorePurchases() {
|
|
2464
|
-
|
|
2465
|
-
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build()
|
|
2466
|
-
) { result, purchases ->
|
|
2467
|
-
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
2468
|
-
val restored = JSONArray()
|
|
2469
|
-
purchases.forEach { purchase ->
|
|
2470
|
-
restored.put(JSONObject().apply {
|
|
2471
|
-
put("productId", purchase.products.firstOrNull())
|
|
2472
|
-
put("orderId", purchase.orderId)
|
|
2473
|
-
})
|
|
2474
|
-
}
|
|
2475
|
-
activity.runOnUiThread {
|
|
2476
|
-
webView.evaluateJavascript("window._craftRestoreResolve && window._craftRestoreResolve($restored)", null)
|
|
2477
|
-
}
|
|
2478
|
-
} else {
|
|
2479
|
-
activity.runOnUiThread {
|
|
2480
|
-
webView.evaluateJavascript("window._craftRestoreReject && window._craftRestoreReject('Restore failed')", null)
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
3065
|
+
if (CraftNative.restorePurchases(activity)) return
|
|
2485
3066
|
|
|
2486
|
-
|
|
3067
|
+
val settled = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
2487
3068
|
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
if (enabled) {
|
|
2492
|
-
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
2493
|
-
} else {
|
|
2494
|
-
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
2495
|
-
}
|
|
2496
|
-
isKeepingAwake = enabled
|
|
3069
|
+
fun resolveRestoreRequest(restored: JSONArray) {
|
|
3070
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
3071
|
+
evaluatePromiseJavascript("window._craftRestoreResolve && window._craftRestoreResolve($restored)")
|
|
2497
3072
|
}
|
|
2498
|
-
return true
|
|
2499
|
-
}
|
|
2500
3073
|
|
|
2501
|
-
|
|
3074
|
+
fun rejectRestoreRequest(message: String) {
|
|
3075
|
+
if (!settled.compareAndSet(false, true) || closed) return
|
|
3076
|
+
rejectRestore(message)
|
|
3077
|
+
}
|
|
2502
3078
|
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
3079
|
+
val connectedClient = restoreBillingClient
|
|
3080
|
+
if (connectedClient != null && connectedClient.isReady) {
|
|
3081
|
+
queryRestoredPurchases(connectedClient, ::resolveRestoreRequest, ::rejectRestoreRequest)
|
|
3082
|
+
return
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
try {
|
|
3086
|
+
val client = connectedClient ?: BillingClient.newBuilder(activity)
|
|
3087
|
+
.setListener { _, _ -> }
|
|
3088
|
+
.enablePendingPurchases()
|
|
3089
|
+
.build()
|
|
3090
|
+
.also { restoreBillingClient = it }
|
|
3091
|
+
|
|
3092
|
+
client.startConnection(object : BillingClientStateListener {
|
|
3093
|
+
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
3094
|
+
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
3095
|
+
queryRestoredPurchases(client, ::resolveRestoreRequest, ::rejectRestoreRequest)
|
|
3096
|
+
} else {
|
|
3097
|
+
rejectRestoreRequest("Billing setup failed: ${billingResult.debugMessage}")
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
override fun onBillingServiceDisconnected() {
|
|
3102
|
+
rejectRestoreRequest("Billing disconnected")
|
|
3103
|
+
}
|
|
3104
|
+
})
|
|
3105
|
+
} catch (error: Exception) {
|
|
3106
|
+
rejectRestoreRequest(error.message ?: "Billing connection failed")
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
private fun queryRestoredPurchases(
|
|
3111
|
+
client: BillingClient,
|
|
3112
|
+
resolve: (JSONArray) -> Unit,
|
|
3113
|
+
reject: (String) -> Unit
|
|
3114
|
+
) {
|
|
3115
|
+
try {
|
|
3116
|
+
client.queryPurchasesAsync(
|
|
3117
|
+
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP).build()
|
|
3118
|
+
) { result, purchases ->
|
|
3119
|
+
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
3120
|
+
val restored = JSONArray()
|
|
3121
|
+
purchases.forEach { purchase ->
|
|
3122
|
+
restored.put(JSONObject().apply {
|
|
3123
|
+
put("productId", purchase.products.firstOrNull())
|
|
3124
|
+
put("orderId", purchase.orderId)
|
|
3125
|
+
})
|
|
3126
|
+
}
|
|
3127
|
+
resolve(restored)
|
|
3128
|
+
} else {
|
|
3129
|
+
reject("Restore failed: ${result.debugMessage}")
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
} catch (error: Exception) {
|
|
3133
|
+
reject(error.message ?: "Restore failed")
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
private fun rejectRestore(message: String) {
|
|
3138
|
+
evaluatePromiseJavascript(
|
|
3139
|
+
"window._craftRestoreReject && window._craftRestoreReject(${jsQuote(message)})"
|
|
3140
|
+
)
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// ==================== Keep Awake ====================
|
|
3144
|
+
|
|
3145
|
+
@JavascriptInterface
|
|
3146
|
+
fun setKeepAwake(enabled: Boolean): Boolean {
|
|
3147
|
+
// Zig queues the same window call on the same looper. isKeepingAwake
|
|
3148
|
+
// below is written here and read nowhere, so the Kotlin body not
|
|
3149
|
+
// running leaves nothing stale — unlike the flashlight pair.
|
|
3150
|
+
if (CraftNative.setKeepAwake(activity, enabled)) return true
|
|
3151
|
+
|
|
3152
|
+
activity.runOnUiThread {
|
|
3153
|
+
if (enabled) {
|
|
3154
|
+
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
3155
|
+
} else {
|
|
3156
|
+
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
3157
|
+
}
|
|
3158
|
+
isKeepingAwake = enabled
|
|
3159
|
+
}
|
|
3160
|
+
return true
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// ==================== Orientation Lock ====================
|
|
3164
|
+
|
|
3165
|
+
@JavascriptInterface
|
|
3166
|
+
fun lockOrientation(orientation: String): Boolean {
|
|
3167
|
+
// Zig queues the same work on the same looper, through a Runnable
|
|
3168
|
+
// this file's holder owns — see CraftNative.runOnMain.
|
|
3169
|
+
if (CraftNative.lockOrientation(activity, orientation)) return true
|
|
3170
|
+
|
|
3171
|
+
activity.runOnUiThread {
|
|
3172
|
+
activity.requestedOrientation = when (orientation) {
|
|
3173
|
+
"portrait" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
2508
3174
|
"landscape" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
2509
3175
|
"landscapeLeft" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
2510
3176
|
"landscapeRight" -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
|
@@ -2516,6 +3182,8 @@ class CraftBridge(
|
|
|
2516
3182
|
|
|
2517
3183
|
@JavascriptInterface
|
|
2518
3184
|
fun unlockOrientation(): Boolean {
|
|
3185
|
+
if (CraftNative.unlockOrientation(activity)) return true
|
|
3186
|
+
|
|
2519
3187
|
activity.runOnUiThread {
|
|
2520
3188
|
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
2521
3189
|
}
|
|
@@ -2539,29 +3207,51 @@ class CraftBridge(
|
|
|
2539
3207
|
@JavascriptInterface
|
|
2540
3208
|
fun scanQRCode() {
|
|
2541
3209
|
// Uses ML Kit barcode scanning - requires camera permission
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
null
|
|
2546
|
-
)
|
|
2547
|
-
}
|
|
3210
|
+
evaluatePromiseJavascript(
|
|
3211
|
+
"window._craftQRReject && window._craftQRReject('QR scanning requires camera integration - use native camera intent')"
|
|
3212
|
+
)
|
|
2548
3213
|
}
|
|
2549
3214
|
|
|
2550
3215
|
// ==================== File Picker ====================
|
|
2551
3216
|
|
|
2552
3217
|
@JavascriptInterface
|
|
2553
3218
|
fun pickFile(typesJson: String) {
|
|
2554
|
-
val
|
|
2555
|
-
|
|
2556
|
-
|
|
3219
|
+
val requestedTypes = try {
|
|
3220
|
+
val values = JSONArray(typesJson)
|
|
3221
|
+
(0 until values.length()).map { index -> values.getString(index) }.also { types ->
|
|
3222
|
+
require(types.all { MIME_TYPE.matches(it) }) { "Invalid file-picker MIME type" }
|
|
3223
|
+
}
|
|
3224
|
+
} catch (error: Exception) {
|
|
3225
|
+
rejectFilePicker(error.message ?: "File-picker types are invalid")
|
|
3226
|
+
return
|
|
3227
|
+
}
|
|
3228
|
+
// The native launcher has no MIME-type argument. It remains the fast
|
|
3229
|
+
// path for an unrestricted picker; filtered requests stay here so the
|
|
3230
|
+
// contract the page asked for reaches Android's document provider.
|
|
3231
|
+
if (requestedTypes.isEmpty() && CraftNative.pickFile(activity)) return
|
|
3232
|
+
|
|
3233
|
+
try {
|
|
3234
|
+
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
3235
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
3236
|
+
type = if (requestedTypes.size == 1) requestedTypes[0] else "*/*"
|
|
3237
|
+
if (requestedTypes.size > 1) {
|
|
3238
|
+
putExtra(Intent.EXTRA_MIME_TYPES, requestedTypes.toTypedArray())
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
activity.startActivityForResult(intent, REQUEST_FILE_PICKER)
|
|
3242
|
+
} catch (error: Exception) {
|
|
3243
|
+
rejectFilePicker(error.message ?: "File picker could not be opened")
|
|
2557
3244
|
}
|
|
2558
|
-
activity.startActivityForResult(intent, REQUEST_FILE_PICKER)
|
|
2559
3245
|
}
|
|
2560
3246
|
|
|
2561
3247
|
// ==================== File Download ====================
|
|
2562
3248
|
|
|
2563
3249
|
@JavascriptInterface
|
|
2564
3250
|
fun downloadFile(url: String, filename: String) {
|
|
3251
|
+
// Zig answers through the reply channel, so returning here is what
|
|
3252
|
+
// stops the promise being settled twice.
|
|
3253
|
+
if (CraftNative.downloadFile(activity, url, filename)) return
|
|
3254
|
+
|
|
2565
3255
|
try {
|
|
2566
3256
|
val request = DownloadManager.Request(Uri.parse(url)).apply {
|
|
2567
3257
|
setTitle(filename)
|
|
@@ -2573,24 +3263,22 @@ class CraftBridge(
|
|
|
2573
3263
|
val downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
2574
3264
|
val downloadId = downloadManager.enqueue(request)
|
|
2575
3265
|
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
null
|
|
2580
|
-
)
|
|
2581
|
-
}
|
|
3266
|
+
evaluatePromiseJavascript(
|
|
3267
|
+
"window._craftDownloadResolve && window._craftDownloadResolve(${jsQuote(downloadId)})"
|
|
3268
|
+
)
|
|
2582
3269
|
} catch (e: Exception) {
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
null
|
|
2587
|
-
)
|
|
2588
|
-
}
|
|
3270
|
+
evaluatePromiseJavascript(
|
|
3271
|
+
"window._craftDownloadReject && window._craftDownloadReject(${jsQuote(e.message ?: "Download could not be started")})"
|
|
3272
|
+
)
|
|
2589
3273
|
}
|
|
2590
3274
|
}
|
|
2591
3275
|
|
|
2592
3276
|
@JavascriptInterface
|
|
2593
3277
|
fun saveFile(data: String, filename: String) {
|
|
3278
|
+
// Zig answers through the reply channel, so returning here is what
|
|
3279
|
+
// stops the promise being settled twice.
|
|
3280
|
+
if (CraftNative.saveFile(activity, data, filename)) return
|
|
3281
|
+
|
|
2594
3282
|
try {
|
|
2595
3283
|
val documentsDir = activity.getExternalFilesDir(android.os.Environment.DIRECTORY_DOCUMENTS)
|
|
2596
3284
|
val file = File(documentsDir, filename)
|
|
@@ -2598,53 +3286,50 @@ class CraftBridge(
|
|
|
2598
3286
|
if (data.startsWith("data:")) {
|
|
2599
3287
|
// Base64 data URL
|
|
2600
3288
|
val parts = data.split(",")
|
|
2601
|
-
if (parts.size
|
|
2602
|
-
|
|
2603
|
-
|
|
3289
|
+
if (parts.size != 2) {
|
|
3290
|
+
rejectSaveFile("Malformed data URL")
|
|
3291
|
+
return
|
|
2604
3292
|
}
|
|
3293
|
+
val bytes = Base64.decode(parts[1], Base64.DEFAULT)
|
|
3294
|
+
file.writeBytes(bytes)
|
|
2605
3295
|
} else {
|
|
2606
3296
|
file.writeText(data)
|
|
2607
3297
|
}
|
|
2608
3298
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
null
|
|
2613
|
-
)
|
|
2614
|
-
}
|
|
3299
|
+
evaluatePromiseJavascript(
|
|
3300
|
+
"window._craftSaveResolve && window._craftSaveResolve(${jsQuote(file.absolutePath)})"
|
|
3301
|
+
)
|
|
2615
3302
|
} catch (e: Exception) {
|
|
2616
|
-
|
|
2617
|
-
webView.evaluateJavascript(
|
|
2618
|
-
"window._craftSaveReject && window._craftSaveReject('${e.message}')",
|
|
2619
|
-
null
|
|
2620
|
-
)
|
|
2621
|
-
}
|
|
3303
|
+
rejectSaveFile(e.message)
|
|
2622
3304
|
}
|
|
2623
3305
|
}
|
|
2624
3306
|
|
|
3307
|
+
private fun rejectSaveFile(message: String?) {
|
|
3308
|
+
evaluatePromiseJavascript(
|
|
3309
|
+
"window._craftSaveReject && window._craftSaveReject(${jsQuote(message ?: "File save failed")})"
|
|
3310
|
+
)
|
|
3311
|
+
}
|
|
3312
|
+
|
|
2625
3313
|
// ==================== Google Sign In ====================
|
|
2626
3314
|
|
|
2627
3315
|
@JavascriptInterface
|
|
2628
3316
|
fun signInWithGoogle() {
|
|
2629
3317
|
// Google Sign In requires configuration in build.gradle
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
null
|
|
2634
|
-
)
|
|
2635
|
-
}
|
|
3318
|
+
evaluatePromiseJavascript(
|
|
3319
|
+
"window._craftGoogleReject && window._craftGoogleReject('Google Sign In requires app configuration')"
|
|
3320
|
+
)
|
|
2636
3321
|
}
|
|
2637
3322
|
|
|
2638
3323
|
// ==================== Audio Recording ====================
|
|
2639
3324
|
|
|
2640
3325
|
@JavascriptInterface
|
|
2641
3326
|
fun startAudioRecording() {
|
|
3327
|
+
if (CraftNative.startAudioRecording(activity)) return
|
|
3328
|
+
|
|
2642
3329
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
|
|
2643
3330
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
webView.evaluateJavascript("window._craftAudioReject && window._craftAudioReject('Permission denied')", null)
|
|
2647
|
-
}
|
|
3331
|
+
evaluatePromiseJavascript("window._craftAudioReject && window._craftAudioReject('Permission denied')")
|
|
3332
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.RECORD_AUDIO), REQUEST_AUDIO)
|
|
2648
3333
|
return
|
|
2649
3334
|
}
|
|
2650
3335
|
|
|
@@ -2668,18 +3353,18 @@ class CraftBridge(
|
|
|
2668
3353
|
start()
|
|
2669
3354
|
}
|
|
2670
3355
|
|
|
2671
|
-
|
|
2672
|
-
webView.evaluateJavascript("window._craftAudioResolve && window._craftAudioResolve(true)", null)
|
|
2673
|
-
}
|
|
3356
|
+
evaluatePromiseJavascript("window._craftAudioResolve && window._craftAudioResolve(true)")
|
|
2674
3357
|
} catch (e: Exception) {
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
3358
|
+
evaluatePromiseJavascript(
|
|
3359
|
+
"window._craftAudioReject && window._craftAudioReject(${jsQuote(e.message ?: "Audio recording could not start")})"
|
|
3360
|
+
)
|
|
2678
3361
|
}
|
|
2679
3362
|
}
|
|
2680
3363
|
|
|
2681
3364
|
@JavascriptInterface
|
|
2682
3365
|
fun stopAudioRecording() {
|
|
3366
|
+
if (CraftNative.stopAudioRecording(activity)) return
|
|
3367
|
+
|
|
2683
3368
|
try {
|
|
2684
3369
|
mediaRecorder?.stop()
|
|
2685
3370
|
mediaRecorder?.release()
|
|
@@ -2688,18 +3373,18 @@ class CraftBridge(
|
|
|
2688
3373
|
audioFile?.let { file ->
|
|
2689
3374
|
val bytes = file.readBytes()
|
|
2690
3375
|
val base64 = "data:audio/m4a;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
3376
|
+
evaluatePromiseJavascript(
|
|
3377
|
+
"window._craftAudioStopResolve && window._craftAudioStopResolve(${jsQuote(base64)})"
|
|
3378
|
+
)
|
|
2694
3379
|
} ?: run {
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
3380
|
+
evaluatePromiseJavascript(
|
|
3381
|
+
"window._craftAudioStopReject && window._craftAudioStopReject('No recording')"
|
|
3382
|
+
)
|
|
2698
3383
|
}
|
|
2699
3384
|
} catch (e: Exception) {
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
3385
|
+
evaluatePromiseJavascript(
|
|
3386
|
+
"window._craftAudioStopReject && window._craftAudioStopReject(${jsQuote(e.message ?: "Audio recording could not stop")})"
|
|
3387
|
+
)
|
|
2703
3388
|
}
|
|
2704
3389
|
}
|
|
2705
3390
|
|
|
@@ -2707,16 +3392,29 @@ class CraftBridge(
|
|
|
2707
3392
|
|
|
2708
3393
|
@JavascriptInterface
|
|
2709
3394
|
fun startVideoRecording() {
|
|
2710
|
-
|
|
2711
|
-
|
|
3395
|
+
if (CraftNative.startVideoRecording(activity)) return
|
|
3396
|
+
|
|
3397
|
+
try {
|
|
3398
|
+
val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply {
|
|
3399
|
+
putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1)
|
|
3400
|
+
}
|
|
3401
|
+
activity.startActivityForResult(intent, REQUEST_VIDEO)
|
|
3402
|
+
} catch (error: Exception) {
|
|
3403
|
+
evaluatePromiseJavascript(
|
|
3404
|
+
"window._craftVideoReject && window._craftVideoReject(${jsQuote(error.message ?: "Video capture could not be opened")})"
|
|
3405
|
+
)
|
|
2712
3406
|
}
|
|
2713
|
-
activity.startActivityForResult(intent, REQUEST_VIDEO)
|
|
2714
3407
|
}
|
|
2715
3408
|
|
|
2716
3409
|
// ==================== Motion Sensors ====================
|
|
2717
3410
|
|
|
2718
3411
|
@JavascriptInterface
|
|
2719
3412
|
fun startMotionUpdates(intervalMs: Int): Boolean {
|
|
3413
|
+
// The SensorEventListener object is CraftNative's, and so is the
|
|
3414
|
+
// last-value bookkeeping — which is why stopMotionUpdates has to reach
|
|
3415
|
+
// the same way, or its own fields stay null and nothing unregisters.
|
|
3416
|
+
if (CraftNative.startMotionUpdates(activity, intervalMs)) return true
|
|
3417
|
+
|
|
2720
3418
|
sensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
2721
3419
|
accelerometer = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
|
2722
3420
|
gyroscope = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
|
@@ -2758,6 +3456,8 @@ class CraftBridge(
|
|
|
2758
3456
|
|
|
2759
3457
|
@JavascriptInterface
|
|
2760
3458
|
fun stopMotionUpdates() {
|
|
3459
|
+
if (CraftNative.stopMotionUpdates(activity)) return
|
|
3460
|
+
|
|
2761
3461
|
sensorListener?.let { sensorManager?.unregisterListener(it) }
|
|
2762
3462
|
sensorListener = null
|
|
2763
3463
|
}
|
|
@@ -2771,24 +3471,29 @@ class CraftBridge(
|
|
|
2771
3471
|
database = activity.openOrCreateDatabase("craft.db", Context.MODE_PRIVATE, null)
|
|
2772
3472
|
}
|
|
2773
3473
|
|
|
3474
|
+
// After the open, so Zig borrows this connection rather than
|
|
3475
|
+
// starting a second one against the same file. Zig answers through
|
|
3476
|
+
// the reply channel, so returning is what stops the promise being
|
|
3477
|
+
// settled twice.
|
|
3478
|
+
database?.let { if (CraftNative.dbExecute(it, sql, paramsJson)) return }
|
|
3479
|
+
|
|
2774
3480
|
val params = JSONArray(paramsJson)
|
|
2775
3481
|
val args = Array(params.length()) { params.getString(it) }
|
|
2776
3482
|
|
|
2777
|
-
database
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
null
|
|
2783
|
-
)
|
|
3483
|
+
val rowsAffected = database!!.compileStatement(sql).use { statement ->
|
|
3484
|
+
args.forEachIndexed { index, value ->
|
|
3485
|
+
statement.bindString(index + 1, value)
|
|
3486
|
+
}
|
|
3487
|
+
statement.executeUpdateDelete()
|
|
2784
3488
|
}
|
|
3489
|
+
|
|
3490
|
+
evaluatePromiseJavascript(
|
|
3491
|
+
"window._craftDbExecResolve && window._craftDbExecResolve({rowsAffected: $rowsAffected})"
|
|
3492
|
+
)
|
|
2785
3493
|
} catch (e: Exception) {
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
null
|
|
2790
|
-
)
|
|
2791
|
-
}
|
|
3494
|
+
evaluatePromiseJavascript(
|
|
3495
|
+
"window._craftDbExecReject && window._craftDbExecReject(${jsQuote(e.message ?: "Database execution failed")})"
|
|
3496
|
+
)
|
|
2792
3497
|
}
|
|
2793
3498
|
}
|
|
2794
3499
|
|
|
@@ -2799,6 +3504,8 @@ class CraftBridge(
|
|
|
2799
3504
|
database = activity.openOrCreateDatabase("craft.db", Context.MODE_PRIVATE, null)
|
|
2800
3505
|
}
|
|
2801
3506
|
|
|
3507
|
+
database?.let { if (CraftNative.dbQuery(it, sql, paramsJson)) return }
|
|
3508
|
+
|
|
2802
3509
|
val params = JSONArray(paramsJson)
|
|
2803
3510
|
val args = Array(params.length()) { params.getString(it) }
|
|
2804
3511
|
|
|
@@ -2816,19 +3523,11 @@ class CraftBridge(
|
|
|
2816
3523
|
}
|
|
2817
3524
|
}
|
|
2818
3525
|
|
|
2819
|
-
|
|
2820
|
-
webView.evaluateJavascript(
|
|
2821
|
-
"window._craftDbQueryResolve && window._craftDbQueryResolve($results)",
|
|
2822
|
-
null
|
|
2823
|
-
)
|
|
2824
|
-
}
|
|
3526
|
+
evaluatePromiseJavascript("window._craftDbQueryResolve && window._craftDbQueryResolve($results)")
|
|
2825
3527
|
} catch (e: Exception) {
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
null
|
|
2830
|
-
)
|
|
2831
|
-
}
|
|
3528
|
+
evaluatePromiseJavascript(
|
|
3529
|
+
"window._craftDbQueryReject && window._craftDbQueryReject(${jsQuote(e.message ?: "Database query failed")})"
|
|
3530
|
+
)
|
|
2832
3531
|
}
|
|
2833
3532
|
}
|
|
2834
3533
|
|
|
@@ -2836,22 +3535,57 @@ class CraftBridge(
|
|
|
2836
3535
|
|
|
2837
3536
|
@JavascriptInterface
|
|
2838
3537
|
fun startBluetoothScan() {
|
|
3538
|
+
// The ScanCallback object is CraftNative's, and so are the scanner and
|
|
3539
|
+
// callback fields — which is why stopBluetoothScan has to reach the
|
|
3540
|
+
// same way, or its own fields stay null and stopScan is never called.
|
|
3541
|
+
if (CraftNative.startBluetoothScan(activity)) return
|
|
3542
|
+
|
|
2839
3543
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_SCAN)
|
|
2840
3544
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
3545
|
+
rejectBluetoothScan("Bluetooth permission denied")
|
|
2841
3546
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
2842
|
-
|
|
2843
|
-
}
|
|
2844
|
-
activity.runOnUiThread {
|
|
2845
|
-
webView.evaluateJavascript("window._craftBleReject && window._craftBleReject('Permission denied')", null)
|
|
3547
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.BLUETOOTH_SCAN), REQUEST_BLUETOOTH)
|
|
2846
3548
|
}
|
|
2847
3549
|
return
|
|
2848
3550
|
}
|
|
2849
3551
|
|
|
2850
|
-
val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
|
2851
|
-
|
|
2852
|
-
|
|
3552
|
+
val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
3553
|
+
if (bluetoothManager == null) {
|
|
3554
|
+
rejectBluetoothScan("Bluetooth is unavailable on this device")
|
|
3555
|
+
return
|
|
3556
|
+
}
|
|
3557
|
+
val adapter = try {
|
|
3558
|
+
bluetoothManager.adapter
|
|
3559
|
+
} catch (e: SecurityException) {
|
|
3560
|
+
rejectBluetoothScan("Bluetooth permission denied")
|
|
3561
|
+
return
|
|
3562
|
+
}
|
|
3563
|
+
if (adapter == null) {
|
|
3564
|
+
rejectBluetoothScan("Bluetooth is unavailable on this device")
|
|
3565
|
+
return
|
|
3566
|
+
}
|
|
3567
|
+
val enabled = try {
|
|
3568
|
+
adapter.isEnabled
|
|
3569
|
+
} catch (e: SecurityException) {
|
|
3570
|
+
rejectBluetoothScan("Bluetooth permission denied")
|
|
3571
|
+
return
|
|
3572
|
+
}
|
|
3573
|
+
if (!enabled) {
|
|
3574
|
+
rejectBluetoothScan("Bluetooth is switched off")
|
|
3575
|
+
return
|
|
3576
|
+
}
|
|
3577
|
+
val scanner = try {
|
|
3578
|
+
adapter.bluetoothLeScanner
|
|
3579
|
+
} catch (e: SecurityException) {
|
|
3580
|
+
rejectBluetoothScan("Bluetooth permission denied")
|
|
3581
|
+
return
|
|
3582
|
+
}
|
|
3583
|
+
if (scanner == null) {
|
|
3584
|
+
rejectBluetoothScan("Bluetooth LE scanning is unavailable")
|
|
3585
|
+
return
|
|
3586
|
+
}
|
|
2853
3587
|
|
|
2854
|
-
|
|
3588
|
+
val callback = object : ScanCallback() {
|
|
2855
3589
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
2856
3590
|
sendEvent("craftBluetoothDevice", mapOf(
|
|
2857
3591
|
"id" to result.device.address,
|
|
@@ -2861,14 +3595,31 @@ class CraftBridge(
|
|
|
2861
3595
|
}
|
|
2862
3596
|
}
|
|
2863
3597
|
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
3598
|
+
try {
|
|
3599
|
+
scanner.startScan(callback)
|
|
3600
|
+
} catch (e: SecurityException) {
|
|
3601
|
+
rejectBluetoothScan("Bluetooth permission denied")
|
|
3602
|
+
return
|
|
3603
|
+
} catch (e: RuntimeException) {
|
|
3604
|
+
rejectBluetoothScan("Bluetooth scan could not start")
|
|
3605
|
+
return
|
|
2867
3606
|
}
|
|
3607
|
+
bluetoothAdapter = adapter
|
|
3608
|
+
bluetoothScanner = scanner
|
|
3609
|
+
bleScanCallback = callback
|
|
3610
|
+
evaluatePromiseJavascript("window._craftBleResolve && window._craftBleResolve(true)")
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
private fun rejectBluetoothScan(message: String) {
|
|
3614
|
+
evaluatePromiseJavascript(
|
|
3615
|
+
"window._craftBleReject && window._craftBleReject(${jsQuote(message)})"
|
|
3616
|
+
)
|
|
2868
3617
|
}
|
|
2869
3618
|
|
|
2870
3619
|
@JavascriptInterface
|
|
2871
3620
|
fun stopBluetoothScan() {
|
|
3621
|
+
if (CraftNative.stopBluetoothScan(activity)) return
|
|
3622
|
+
|
|
2872
3623
|
bleScanCallback?.let { bluetoothScanner?.stopScan(it) }
|
|
2873
3624
|
bleScanCallback = null
|
|
2874
3625
|
}
|
|
@@ -2879,15 +3630,13 @@ class CraftBridge(
|
|
|
2879
3630
|
fun scanNFC() {
|
|
2880
3631
|
val nfcAdapter = NfcAdapter.getDefaultAdapter(activity)
|
|
2881
3632
|
if (nfcAdapter == null || !nfcAdapter.isEnabled) {
|
|
2882
|
-
|
|
2883
|
-
webView.evaluateJavascript("window._craftNfcReject && window._craftNfcReject('NFC not available')", null)
|
|
2884
|
-
}
|
|
3633
|
+
evaluatePromiseJavascript("window._craftNfcReject && window._craftNfcReject('NFC not available')")
|
|
2885
3634
|
return
|
|
2886
3635
|
}
|
|
2887
3636
|
// NFC requires foreground dispatch - simplified implementation
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
3637
|
+
evaluatePromiseJavascript(
|
|
3638
|
+
"window._craftNfcReject && window._craftNfcReject('NFC requires activity integration')"
|
|
3639
|
+
)
|
|
2891
3640
|
}
|
|
2892
3641
|
|
|
2893
3642
|
// ==================== Fitness ====================
|
|
@@ -2907,32 +3656,137 @@ class CraftBridge(
|
|
|
2907
3656
|
healthConnect.saveWorkout(workoutJson)
|
|
2908
3657
|
}
|
|
2909
3658
|
|
|
2910
|
-
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean
|
|
2911
|
-
|
|
3659
|
+
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
|
3660
|
+
if (closed) return requestCode == REQUEST_CAMERA || requestCode == REQUEST_GALLERY ||
|
|
3661
|
+
requestCode == REQUEST_FILE_PICKER || requestCode == REQUEST_VIDEO ||
|
|
3662
|
+
requestCode == REQUEST_PICK_CONTACT
|
|
3663
|
+
|
|
3664
|
+
return when (requestCode) {
|
|
3665
|
+
REQUEST_CAMERA, REQUEST_GALLERY -> {
|
|
3666
|
+
handleImageResult(requestCode, resultCode, data)
|
|
3667
|
+
true
|
|
3668
|
+
}
|
|
3669
|
+
REQUEST_FILE_PICKER -> {
|
|
3670
|
+
handleFilePickerResult(resultCode, data)
|
|
3671
|
+
true
|
|
3672
|
+
}
|
|
3673
|
+
REQUEST_VIDEO -> {
|
|
3674
|
+
handleVideoResult(resultCode, data)
|
|
3675
|
+
true
|
|
3676
|
+
}
|
|
3677
|
+
REQUEST_PICK_CONTACT -> {
|
|
3678
|
+
handleContactPickerResult(resultCode, data)
|
|
3679
|
+
true
|
|
3680
|
+
}
|
|
3681
|
+
else -> healthConnect.onActivityResult(requestCode, resultCode, data)
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
2912
3684
|
|
|
2913
3685
|
fun close() {
|
|
3686
|
+
closed = true
|
|
3687
|
+
currentPositionSequence.incrementAndGet()
|
|
3688
|
+
activity.runOnUiThread {
|
|
3689
|
+
runCatching {
|
|
3690
|
+
webView.evaluateJavascript(
|
|
3691
|
+
"window.__craftRejectPendingPromises && window.__craftRejectPendingPromises('Android bridge closed');" +
|
|
3692
|
+
"window.__craftRejectPermissionRequests && window.__craftRejectPermissionRequests('Android bridge closed')",
|
|
3693
|
+
null
|
|
3694
|
+
)
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
CraftNative.close(activity)
|
|
2914
3698
|
healthConnect.close()
|
|
3699
|
+
|
|
3700
|
+
runCatching { speechRecognizer?.cancel() }
|
|
3701
|
+
runCatching { speechRecognizer?.destroy() }
|
|
3702
|
+
speechRecognizer = null
|
|
3703
|
+
runCatching { biometricPrompt?.cancelAuthentication() }
|
|
3704
|
+
biometricPrompt = null
|
|
3705
|
+
|
|
3706
|
+
mediaRecorder?.let { recorder ->
|
|
3707
|
+
runCatching { recorder.stop() }
|
|
3708
|
+
runCatching { recorder.release() }
|
|
3709
|
+
}
|
|
3710
|
+
mediaRecorder = null
|
|
3711
|
+
audioFile?.delete()
|
|
3712
|
+
audioFile = null
|
|
3713
|
+
|
|
3714
|
+
locationCallbacks.values.forEach { callback ->
|
|
3715
|
+
runCatching { fusedLocationClient?.removeLocationUpdates(callback) }
|
|
3716
|
+
}
|
|
3717
|
+
locationCallbacks.clear()
|
|
3718
|
+
oneShotLocationCallbacks.forEach { callback ->
|
|
3719
|
+
runCatching { fusedLocationClient?.removeLocationUpdates(callback) }
|
|
3720
|
+
}
|
|
3721
|
+
oneShotLocationCallbacks.clear()
|
|
3722
|
+
oneShotLocationTimeouts.values.forEach { timeout ->
|
|
3723
|
+
oneShotLocationHandler.removeCallbacks(timeout)
|
|
3724
|
+
}
|
|
3725
|
+
oneShotLocationTimeouts.clear()
|
|
3726
|
+
|
|
3727
|
+
networkCallback?.let { callback ->
|
|
3728
|
+
val manager = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
3729
|
+
runCatching { manager.unregisterNetworkCallback(callback) }
|
|
3730
|
+
}
|
|
3731
|
+
networkCallback = null
|
|
3732
|
+
ProcessLifecycleOwner.get().lifecycle.removeObserver(appStateObserver)
|
|
3733
|
+
|
|
3734
|
+
sensorListener?.let { listener ->
|
|
3735
|
+
runCatching { sensorManager?.unregisterListener(listener) }
|
|
3736
|
+
}
|
|
3737
|
+
sensorListener = null
|
|
3738
|
+
sensorManager = null
|
|
3739
|
+
|
|
3740
|
+
bleScanCallback?.let { callback ->
|
|
3741
|
+
runCatching { bluetoothScanner?.stopScan(callback) }
|
|
3742
|
+
}
|
|
3743
|
+
bleScanCallback = null
|
|
3744
|
+
bluetoothScanner = null
|
|
3745
|
+
bluetoothAdapter = null
|
|
3746
|
+
|
|
3747
|
+
runCatching { productBillingClient?.endConnection() }
|
|
3748
|
+
productBillingClient = null
|
|
3749
|
+
runCatching { restoreBillingClient?.endConnection() }
|
|
3750
|
+
restoreBillingClient = null
|
|
3751
|
+
runCatching { database?.close() }
|
|
3752
|
+
database = null
|
|
3753
|
+
|
|
3754
|
+
if (isFlashlightOn) setFlashlight(false)
|
|
3755
|
+
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
3756
|
+
isKeepingAwake = false
|
|
3757
|
+
pendingPermissionRequests.clear()
|
|
3758
|
+
pendingEvents.clear()
|
|
2915
3759
|
}
|
|
2916
3760
|
|
|
2917
3761
|
// ==================== Screen Capture ====================
|
|
2918
3762
|
|
|
2919
3763
|
@JavascriptInterface
|
|
2920
3764
|
fun takeScreenshot() {
|
|
3765
|
+
if (CraftNative.takeScreenshot(activity, webView)) return
|
|
3766
|
+
|
|
2921
3767
|
activity.runOnUiThread {
|
|
3768
|
+
if (closed) return@runOnUiThread
|
|
2922
3769
|
try {
|
|
2923
3770
|
val view = webView.rootView
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3771
|
+
val bitmap = try {
|
|
3772
|
+
view.isDrawingCacheEnabled = true
|
|
3773
|
+
view.buildDrawingCache()
|
|
3774
|
+
Bitmap.createBitmap(view.drawingCache)
|
|
3775
|
+
} finally {
|
|
3776
|
+
view.isDrawingCacheEnabled = false
|
|
3777
|
+
}
|
|
2928
3778
|
|
|
2929
3779
|
val outputStream = ByteArrayOutputStream()
|
|
2930
3780
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
|
2931
3781
|
val base64 = "data:image/png;base64," + Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
|
2932
3782
|
|
|
2933
|
-
|
|
3783
|
+
evaluatePromiseJavascript(
|
|
3784
|
+
"window._craftScreenshotResolve && window._craftScreenshotResolve(${jsQuote(base64)})"
|
|
3785
|
+
)
|
|
2934
3786
|
} catch (e: Exception) {
|
|
2935
|
-
|
|
3787
|
+
evaluatePromiseJavascript(
|
|
3788
|
+
"window._craftScreenshotReject && window._craftScreenshotReject(${jsQuote(e.message ?: "Screenshot failed")})"
|
|
3789
|
+
)
|
|
2936
3790
|
}
|
|
2937
3791
|
}
|
|
2938
3792
|
}
|
|
@@ -2941,89 +3795,44 @@ class CraftBridge(
|
|
|
2941
3795
|
|
|
2942
3796
|
@JavascriptInterface
|
|
2943
3797
|
fun registerBackgroundTask(taskId: String) {
|
|
2944
|
-
|
|
2945
|
-
// WorkManager tasks don't require explicit registration, just track them
|
|
2946
|
-
activity.runOnUiThread {
|
|
2947
|
-
webView.evaluateJavascript(
|
|
2948
|
-
"window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', registered: true})",
|
|
2949
|
-
null
|
|
2950
|
-
)
|
|
2951
|
-
}
|
|
2952
|
-
} catch (e: Exception) {
|
|
2953
|
-
activity.runOnUiThread {
|
|
2954
|
-
webView.evaluateJavascript(
|
|
2955
|
-
"window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
|
|
2956
|
-
null
|
|
2957
|
-
)
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
3798
|
+
rejectBackgroundTask(taskId)
|
|
2960
3799
|
}
|
|
2961
3800
|
|
|
2962
3801
|
@JavascriptInterface
|
|
3802
|
+
@Suppress("UNUSED_PARAMETER")
|
|
2963
3803
|
fun scheduleBackgroundTask(taskId: String, delay: Long, requiresNetwork: Boolean, requiresCharging: Boolean) {
|
|
2964
|
-
|
|
2965
|
-
// Note: Full WorkManager implementation requires adding the dependency
|
|
2966
|
-
// implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
|
2967
|
-
// This is a placeholder that shows the API structure
|
|
2968
|
-
activity.runOnUiThread {
|
|
2969
|
-
webView.evaluateJavascript(
|
|
2970
|
-
"window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', scheduled: true, delay: $delay})",
|
|
2971
|
-
null
|
|
2972
|
-
)
|
|
2973
|
-
}
|
|
2974
|
-
} catch (e: Exception) {
|
|
2975
|
-
activity.runOnUiThread {
|
|
2976
|
-
webView.evaluateJavascript(
|
|
2977
|
-
"window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
|
|
2978
|
-
null
|
|
2979
|
-
)
|
|
2980
|
-
}
|
|
2981
|
-
}
|
|
3804
|
+
rejectBackgroundTask(taskId)
|
|
2982
3805
|
}
|
|
2983
3806
|
|
|
2984
3807
|
@JavascriptInterface
|
|
2985
3808
|
fun cancelBackgroundTask(taskId: String) {
|
|
2986
|
-
|
|
2987
|
-
activity.runOnUiThread {
|
|
2988
|
-
webView.evaluateJavascript(
|
|
2989
|
-
"window._craftBgTaskResolve && window._craftBgTaskResolve({taskId: '$taskId', cancelled: true})",
|
|
2990
|
-
null
|
|
2991
|
-
)
|
|
2992
|
-
}
|
|
2993
|
-
} catch (e: Exception) {
|
|
2994
|
-
activity.runOnUiThread {
|
|
2995
|
-
webView.evaluateJavascript(
|
|
2996
|
-
"window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
|
|
2997
|
-
null
|
|
2998
|
-
)
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3809
|
+
rejectBackgroundTask(taskId)
|
|
3001
3810
|
}
|
|
3002
3811
|
|
|
3003
3812
|
@JavascriptInterface
|
|
3004
3813
|
fun cancelAllBackgroundTasks() {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
activity.runOnUiThread {
|
|
3014
|
-
webView.evaluateJavascript(
|
|
3015
|
-
"window._craftBgTaskReject && window._craftBgTaskReject('${e.message}')",
|
|
3016
|
-
null
|
|
3017
|
-
)
|
|
3018
|
-
}
|
|
3814
|
+
rejectBackgroundTask()
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
private fun rejectBackgroundTask(taskId: String? = null) {
|
|
3818
|
+
val message = if (taskId == null) {
|
|
3819
|
+
"Background tasks are unavailable on Android"
|
|
3820
|
+
} else {
|
|
3821
|
+
"Background task $taskId is unavailable on Android"
|
|
3019
3822
|
}
|
|
3823
|
+
evaluatePromiseJavascript(
|
|
3824
|
+
"window._craftBgTaskReject && window._craftBgTaskReject(${jsQuote(message)})"
|
|
3825
|
+
)
|
|
3020
3826
|
}
|
|
3021
3827
|
|
|
3022
3828
|
// ==================== PDF Viewer ====================
|
|
3023
3829
|
|
|
3024
3830
|
@JavascriptInterface
|
|
3025
3831
|
fun openPDF(source: String, page: Int) {
|
|
3832
|
+
if (CraftNative.openPDF(activity, source, page)) return
|
|
3833
|
+
|
|
3026
3834
|
activity.runOnUiThread {
|
|
3835
|
+
if (closed) return@runOnUiThread
|
|
3027
3836
|
try {
|
|
3028
3837
|
val uri = if (source.startsWith("data:")) {
|
|
3029
3838
|
// Save base64 to temp file and open
|
|
@@ -3043,14 +3852,12 @@ class CraftBridge(
|
|
|
3043
3852
|
}
|
|
3044
3853
|
|
|
3045
3854
|
activity.startActivity(intent)
|
|
3046
|
-
|
|
3047
|
-
"window._craftPDFResolve && window._craftPDFResolve({opened: true})"
|
|
3048
|
-
null
|
|
3855
|
+
evaluatePromiseJavascript(
|
|
3856
|
+
"window._craftPDFResolve && window._craftPDFResolve({opened: true})"
|
|
3049
3857
|
)
|
|
3050
3858
|
} catch (e: Exception) {
|
|
3051
|
-
|
|
3052
|
-
"window._craftPDFReject && window._craftPDFReject(
|
|
3053
|
-
null
|
|
3859
|
+
evaluatePromiseJavascript(
|
|
3860
|
+
"window._craftPDFReject && window._craftPDFReject(${jsQuote(e.message ?: "PDF could not be opened")})"
|
|
3054
3861
|
)
|
|
3055
3862
|
}
|
|
3056
3863
|
}
|
|
@@ -3059,38 +3866,39 @@ class CraftBridge(
|
|
|
3059
3866
|
@JavascriptInterface
|
|
3060
3867
|
fun closePDF() {
|
|
3061
3868
|
// PDFs are opened in external viewers, so we just acknowledge
|
|
3062
|
-
|
|
3063
|
-
webView.evaluateJavascript(
|
|
3064
|
-
"window._craftPDFResolve && window._craftPDFResolve(true)",
|
|
3065
|
-
null
|
|
3066
|
-
)
|
|
3067
|
-
}
|
|
3869
|
+
evaluatePromiseJavascript("window._craftPDFResolve && window._craftPDFResolve(true)")
|
|
3068
3870
|
}
|
|
3069
3871
|
|
|
3070
3872
|
// ==================== Contacts Picker ====================
|
|
3071
3873
|
|
|
3072
3874
|
@JavascriptInterface
|
|
3073
3875
|
fun pickContact(multiple: Boolean) {
|
|
3876
|
+
// `multiple` is ignored by both implementations: Android still opens
|
|
3877
|
+
// the platform's single-contact ACTION_PICK surface.
|
|
3074
3878
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS)
|
|
3075
3879
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
3076
|
-
|
|
3880
|
+
rejectContactPicker("Contacts permission is required; retry after granting it")
|
|
3881
|
+
requestPermissionsBestEffort(arrayOf(Manifest.permission.READ_CONTACTS), REQUEST_PICK_CONTACT)
|
|
3077
3882
|
return
|
|
3078
3883
|
}
|
|
3079
3884
|
|
|
3885
|
+
if (CraftNative.pickContact(activity)) return
|
|
3886
|
+
|
|
3080
3887
|
activity.runOnUiThread {
|
|
3081
|
-
|
|
3082
|
-
|
|
3888
|
+
if (closed) return@runOnUiThread
|
|
3889
|
+
try {
|
|
3890
|
+
val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
|
|
3891
|
+
activity.startActivityForResult(intent, REQUEST_PICK_CONTACT)
|
|
3892
|
+
} catch (error: Exception) {
|
|
3893
|
+
rejectContactPicker(error.message ?: "Contact picker could not be opened")
|
|
3894
|
+
}
|
|
3083
3895
|
}
|
|
3084
3896
|
}
|
|
3085
3897
|
|
|
3086
3898
|
fun handleContactPickerResult(resultCode: Int, data: Intent?) {
|
|
3899
|
+
if (closed) return
|
|
3087
3900
|
if (resultCode != Activity.RESULT_OK || data?.data == null) {
|
|
3088
|
-
|
|
3089
|
-
webView.evaluateJavascript(
|
|
3090
|
-
"window._craftPickContactReject && window._craftPickContactReject('Cancelled')",
|
|
3091
|
-
null
|
|
3092
|
-
)
|
|
3093
|
-
}
|
|
3901
|
+
rejectContactPicker("Cancelled")
|
|
3094
3902
|
return
|
|
3095
3903
|
}
|
|
3096
3904
|
|
|
@@ -3101,77 +3909,94 @@ class CraftBridge(
|
|
|
3101
3909
|
ContactsContract.Contacts.DISPLAY_NAME
|
|
3102
3910
|
)
|
|
3103
3911
|
|
|
3104
|
-
activity.contentResolver.query(contactUri, projection, null, null, null)?.use { cursor ->
|
|
3105
|
-
if (cursor.moveToFirst())
|
|
3106
|
-
val contactId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
|
|
3107
|
-
val displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
|
|
3108
|
-
|
|
3109
|
-
// Get phone numbers
|
|
3110
|
-
val phones = mutableListOf<Map<String, String>>()
|
|
3111
|
-
activity.contentResolver.query(
|
|
3112
|
-
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
|
3113
|
-
arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.TYPE),
|
|
3114
|
-
"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
|
|
3115
|
-
arrayOf(contactId),
|
|
3116
|
-
null
|
|
3117
|
-
)?.use { phoneCursor ->
|
|
3118
|
-
while (phoneCursor.moveToNext()) {
|
|
3119
|
-
val number = phoneCursor.getString(phoneCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
|
|
3120
|
-
phones.add(mapOf("number" to number))
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3912
|
+
val contact = activity.contentResolver.query(contactUri, projection, null, null, null)?.use { cursor ->
|
|
3913
|
+
if (!cursor.moveToFirst()) return@use null
|
|
3123
3914
|
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
activity.contentResolver.query(
|
|
3127
|
-
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
|
|
3128
|
-
arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS),
|
|
3129
|
-
"${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?",
|
|
3130
|
-
arrayOf(contactId),
|
|
3131
|
-
null
|
|
3132
|
-
)?.use { emailCursor ->
|
|
3133
|
-
while (emailCursor.moveToNext()) {
|
|
3134
|
-
val address = emailCursor.getString(emailCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS))
|
|
3135
|
-
emails.add(mapOf("address" to address))
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3915
|
+
val contactId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
|
|
3916
|
+
val displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
|
|
3138
3917
|
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3918
|
+
// Get phone numbers
|
|
3919
|
+
val phones = mutableListOf<Map<String, String>>()
|
|
3920
|
+
activity.contentResolver.query(
|
|
3921
|
+
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
|
3922
|
+
arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.TYPE),
|
|
3923
|
+
"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
|
|
3924
|
+
arrayOf(contactId),
|
|
3925
|
+
null
|
|
3926
|
+
)?.use { phoneCursor ->
|
|
3927
|
+
while (phoneCursor.moveToNext()) {
|
|
3928
|
+
val number = phoneCursor.getString(phoneCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
|
|
3929
|
+
phones.add(mapOf("number" to number))
|
|
3144
3930
|
}
|
|
3931
|
+
}
|
|
3145
3932
|
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3933
|
+
// Get emails
|
|
3934
|
+
val emails = mutableListOf<Map<String, String>>()
|
|
3935
|
+
activity.contentResolver.query(
|
|
3936
|
+
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
|
|
3937
|
+
arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS),
|
|
3938
|
+
"${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?",
|
|
3939
|
+
arrayOf(contactId),
|
|
3940
|
+
null
|
|
3941
|
+
)?.use { emailCursor ->
|
|
3942
|
+
while (emailCursor.moveToNext()) {
|
|
3943
|
+
val address = emailCursor.getString(emailCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS))
|
|
3944
|
+
emails.add(mapOf("address" to address))
|
|
3151
3945
|
}
|
|
3152
3946
|
}
|
|
3947
|
+
|
|
3948
|
+
JSONObject().apply {
|
|
3949
|
+
put("id", contactId)
|
|
3950
|
+
put("displayName", displayName)
|
|
3951
|
+
put("phoneNumbers", JSONArray(phones.map { JSONObject(it) }))
|
|
3952
|
+
put("emailAddresses", JSONArray(emails.map { JSONObject(it) }))
|
|
3953
|
+
}
|
|
3153
3954
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
null
|
|
3159
|
-
)
|
|
3955
|
+
|
|
3956
|
+
if (contact == null) {
|
|
3957
|
+
rejectContactPicker("Selected contact could not be read")
|
|
3958
|
+
return
|
|
3160
3959
|
}
|
|
3960
|
+
|
|
3961
|
+
evaluatePromiseJavascript(
|
|
3962
|
+
"window._craftPickContactResolve && window._craftPickContactResolve($contact)"
|
|
3963
|
+
)
|
|
3964
|
+
} catch (e: Exception) {
|
|
3965
|
+
evaluatePromiseJavascript(
|
|
3966
|
+
"window._craftPickContactReject && window._craftPickContactReject(${jsQuote(e.message ?: "Selected contact could not be read")})"
|
|
3967
|
+
)
|
|
3161
3968
|
}
|
|
3162
3969
|
}
|
|
3163
3970
|
|
|
3971
|
+
private fun rejectContactPicker(message: String) {
|
|
3972
|
+
evaluatePromiseJavascript(
|
|
3973
|
+
"window._craftPickContactReject && window._craftPickContactReject(${jsQuote(message)})"
|
|
3974
|
+
)
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3164
3977
|
// ==================== App Shortcuts ====================
|
|
3165
3978
|
|
|
3979
|
+
fun dispatchShortcut(type: String) {
|
|
3980
|
+
activity.runOnUiThread {
|
|
3981
|
+
val data = mapOf<String, Any>("type" to type)
|
|
3982
|
+
if (bridgeReady) {
|
|
3983
|
+
sendEvent("craftShortcut", data)
|
|
3984
|
+
} else {
|
|
3985
|
+
pendingEvents.add("craftShortcut" to data)
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3166
3990
|
@JavascriptInterface
|
|
3167
3991
|
fun setShortcuts(shortcutsJson: String) {
|
|
3992
|
+
// Zig answers through the reply channel, so returning here is what
|
|
3993
|
+
// stops the promise being settled twice.
|
|
3994
|
+
if (CraftNative.setShortcuts(activity, shortcutsJson)) return
|
|
3995
|
+
|
|
3168
3996
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
null
|
|
3173
|
-
)
|
|
3174
|
-
}
|
|
3997
|
+
evaluatePromiseJavascript(
|
|
3998
|
+
"window._craftShortcutsReject && window._craftShortcutsReject('App shortcuts require Android 7.1+')"
|
|
3999
|
+
)
|
|
3175
4000
|
return
|
|
3176
4001
|
}
|
|
3177
4002
|
|
|
@@ -3202,50 +4027,33 @@ class CraftBridge(
|
|
|
3202
4027
|
|
|
3203
4028
|
shortcutManager?.dynamicShortcuts = shortcuts
|
|
3204
4029
|
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
null
|
|
3209
|
-
)
|
|
3210
|
-
}
|
|
4030
|
+
evaluatePromiseJavascript(
|
|
4031
|
+
"window._craftShortcutsResolve && window._craftShortcutsResolve({set: true, count: ${shortcuts.size}})"
|
|
4032
|
+
)
|
|
3211
4033
|
} catch (e: Exception) {
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
null
|
|
3216
|
-
)
|
|
3217
|
-
}
|
|
4034
|
+
evaluatePromiseJavascript(
|
|
4035
|
+
"window._craftShortcutsReject && window._craftShortcutsReject(${jsQuote(e.message ?: "Shortcuts could not be set")})"
|
|
4036
|
+
)
|
|
3218
4037
|
}
|
|
3219
4038
|
}
|
|
3220
4039
|
|
|
3221
4040
|
@JavascriptInterface
|
|
3222
4041
|
fun clearShortcuts() {
|
|
4042
|
+
if (CraftNative.clearShortcuts(activity)) return
|
|
4043
|
+
|
|
3223
4044
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
|
3224
4045
|
try {
|
|
3225
4046
|
val shortcutManager = activity.getSystemService(android.content.pm.ShortcutManager::class.java)
|
|
3226
4047
|
shortcutManager?.removeAllDynamicShortcuts()
|
|
3227
4048
|
|
|
3228
|
-
|
|
3229
|
-
webView.evaluateJavascript(
|
|
3230
|
-
"window._craftShortcutsResolve && window._craftShortcutsResolve(true)",
|
|
3231
|
-
null
|
|
3232
|
-
)
|
|
3233
|
-
}
|
|
4049
|
+
evaluatePromiseJavascript("window._craftShortcutsResolve && window._craftShortcutsResolve({cleared: true})")
|
|
3234
4050
|
} catch (e: Exception) {
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
"window._craftShortcutsReject && window._craftShortcutsReject('${e.message}')",
|
|
3238
|
-
null
|
|
3239
|
-
)
|
|
3240
|
-
}
|
|
3241
|
-
}
|
|
3242
|
-
} else {
|
|
3243
|
-
activity.runOnUiThread {
|
|
3244
|
-
webView.evaluateJavascript(
|
|
3245
|
-
"window._craftShortcutsResolve && window._craftShortcutsResolve(true)",
|
|
3246
|
-
null
|
|
4051
|
+
evaluatePromiseJavascript(
|
|
4052
|
+
"window._craftShortcutsReject && window._craftShortcutsReject(${jsQuote(e.message ?: "Shortcuts could not be cleared")})"
|
|
3247
4053
|
)
|
|
3248
4054
|
}
|
|
4055
|
+
} else {
|
|
4056
|
+
evaluatePromiseJavascript("window._craftShortcutsResolve && window._craftShortcutsResolve({cleared: true})")
|
|
3249
4057
|
}
|
|
3250
4058
|
}
|
|
3251
4059
|
|
|
@@ -3253,111 +4061,95 @@ class CraftBridge(
|
|
|
3253
4061
|
|
|
3254
4062
|
@JavascriptInterface
|
|
3255
4063
|
fun setSharedItem(key: String, value: String, group: String) {
|
|
4064
|
+
// Zig answers through the reply channel, so returning here is what
|
|
4065
|
+
// stops the promise being settled twice.
|
|
4066
|
+
if (CraftNative.setSharedItem(activity, key, value, group)) return
|
|
4067
|
+
|
|
3256
4068
|
try {
|
|
3257
4069
|
val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
|
|
3258
4070
|
val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
3259
4071
|
prefs.edit().putString(key, value).apply()
|
|
3260
4072
|
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
null
|
|
3265
|
-
)
|
|
3266
|
-
}
|
|
4073
|
+
evaluatePromiseJavascript(
|
|
4074
|
+
"window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({set: true, key: ${jsQuote(key)}})"
|
|
4075
|
+
)
|
|
3267
4076
|
} catch (e: Exception) {
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
null
|
|
3272
|
-
)
|
|
3273
|
-
}
|
|
4077
|
+
evaluatePromiseJavascript(
|
|
4078
|
+
"window._craftSharedKeychainReject && window._craftSharedKeychainReject(${jsQuote(e.message ?: "Shared item could not be stored")})"
|
|
4079
|
+
)
|
|
3274
4080
|
}
|
|
3275
4081
|
}
|
|
3276
4082
|
|
|
3277
4083
|
@JavascriptInterface
|
|
3278
4084
|
fun getSharedItem(key: String, group: String) {
|
|
4085
|
+
// Zig answers through the reply channel, so returning here is what
|
|
4086
|
+
// stops the promise being settled twice.
|
|
4087
|
+
if (CraftNative.getSharedItem(activity, key, group)) return
|
|
4088
|
+
|
|
3279
4089
|
try {
|
|
3280
4090
|
val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
|
|
3281
4091
|
val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
3282
4092
|
val value = prefs.getString(key, null)
|
|
3283
4093
|
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
null
|
|
3289
|
-
)
|
|
3290
|
-
} else {
|
|
3291
|
-
webView.evaluateJavascript(
|
|
3292
|
-
"window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({value: null, key: '$key'})",
|
|
3293
|
-
null
|
|
3294
|
-
)
|
|
3295
|
-
}
|
|
4094
|
+
val payload = if (value != null) {
|
|
4095
|
+
"{value: ${jsQuote(value)}, key: ${jsQuote(key)}}"
|
|
4096
|
+
} else {
|
|
4097
|
+
"{value: null, key: ${jsQuote(key)}}"
|
|
3296
4098
|
}
|
|
4099
|
+
evaluatePromiseJavascript(
|
|
4100
|
+
"window._craftSharedKeychainResolve && window._craftSharedKeychainResolve($payload)"
|
|
4101
|
+
)
|
|
3297
4102
|
} catch (e: Exception) {
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
null
|
|
3302
|
-
)
|
|
3303
|
-
}
|
|
4103
|
+
evaluatePromiseJavascript(
|
|
4104
|
+
"window._craftSharedKeychainReject && window._craftSharedKeychainReject(${jsQuote(e.message ?: "Shared item could not be read")})"
|
|
4105
|
+
)
|
|
3304
4106
|
}
|
|
3305
4107
|
}
|
|
3306
4108
|
|
|
3307
4109
|
@JavascriptInterface
|
|
3308
4110
|
fun removeSharedItem(key: String, group: String) {
|
|
4111
|
+
// Zig answers through the reply channel, so returning here is what
|
|
4112
|
+
// stops the promise being settled twice.
|
|
4113
|
+
if (CraftNative.removeSharedItem(activity, key, group)) return
|
|
4114
|
+
|
|
3309
4115
|
try {
|
|
3310
4116
|
val prefsName = if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"
|
|
3311
4117
|
val prefs = activity.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
3312
4118
|
prefs.edit().remove(key).apply()
|
|
3313
4119
|
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
null
|
|
3318
|
-
)
|
|
3319
|
-
}
|
|
4120
|
+
evaluatePromiseJavascript(
|
|
4121
|
+
"window._craftSharedKeychainResolve && window._craftSharedKeychainResolve({removed: true, key: ${jsQuote(key)}})"
|
|
4122
|
+
)
|
|
3320
4123
|
} catch (e: Exception) {
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
null
|
|
3325
|
-
)
|
|
3326
|
-
}
|
|
4124
|
+
evaluatePromiseJavascript(
|
|
4125
|
+
"window._craftSharedKeychainReject && window._craftSharedKeychainReject(${jsQuote(e.message ?: "Shared item could not be removed")})"
|
|
4126
|
+
)
|
|
3327
4127
|
}
|
|
3328
4128
|
}
|
|
3329
4129
|
|
|
3330
4130
|
// ==================== Local Auth Persistence ====================
|
|
3331
4131
|
|
|
3332
|
-
private
|
|
4132
|
+
private val authSessionExpiryKey = "craft_auth_session_expiry"
|
|
3333
4133
|
|
|
3334
4134
|
@JavascriptInterface
|
|
3335
4135
|
fun setAuthPersistence(enabled: Boolean, duration: Long) {
|
|
3336
4136
|
try {
|
|
3337
4137
|
if (enabled) {
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
)
|
|
3344
|
-
}
|
|
4138
|
+
val expiresAt = System.currentTimeMillis() + (duration * 1000)
|
|
4139
|
+
securePrefs.edit().putLong(authSessionExpiryKey, expiresAt).apply()
|
|
4140
|
+
evaluatePromiseJavascript(
|
|
4141
|
+
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({enabled: true, duration: $duration, expiresAt: $expiresAt})"
|
|
4142
|
+
)
|
|
3345
4143
|
} else {
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({enabled: false})",
|
|
3350
|
-
null
|
|
3351
|
-
)
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
} catch (e: Exception) {
|
|
3355
|
-
activity.runOnUiThread {
|
|
3356
|
-
webView.evaluateJavascript(
|
|
3357
|
-
"window._craftAuthPersistReject && window._craftAuthPersistReject('${e.message}')",
|
|
3358
|
-
null
|
|
4144
|
+
securePrefs.edit().remove(authSessionExpiryKey).apply()
|
|
4145
|
+
evaluatePromiseJavascript(
|
|
4146
|
+
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({enabled: false})"
|
|
3359
4147
|
)
|
|
3360
4148
|
}
|
|
4149
|
+
} catch (e: Exception) {
|
|
4150
|
+
evaluatePromiseJavascript(
|
|
4151
|
+
"window._craftAuthPersistReject && window._craftAuthPersistReject(${jsQuote(e.message ?: "Auth persistence could not be updated")})"
|
|
4152
|
+
)
|
|
3361
4153
|
}
|
|
3362
4154
|
}
|
|
3363
4155
|
|
|
@@ -3365,43 +4157,32 @@ class CraftBridge(
|
|
|
3365
4157
|
fun checkAuthPersistence() {
|
|
3366
4158
|
try {
|
|
3367
4159
|
val now = System.currentTimeMillis()
|
|
4160
|
+
val authSessionExpiry = securePrefs.getLong(authSessionExpiryKey, 0L)
|
|
3368
4161
|
val isValid = authSessionExpiry > now
|
|
3369
4162
|
val remainingMs = if (isValid) authSessionExpiry - now else 0L
|
|
3370
4163
|
val remainingSeconds = remainingMs / 1000
|
|
3371
4164
|
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
null
|
|
3376
|
-
)
|
|
3377
|
-
}
|
|
4165
|
+
evaluatePromiseJavascript(
|
|
4166
|
+
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({isValid: $isValid, remainingSeconds: $remainingSeconds})"
|
|
4167
|
+
)
|
|
3378
4168
|
} catch (e: Exception) {
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
null
|
|
3383
|
-
)
|
|
3384
|
-
}
|
|
4169
|
+
evaluatePromiseJavascript(
|
|
4170
|
+
"window._craftAuthPersistReject && window._craftAuthPersistReject(${jsQuote(e.message ?: "Auth persistence could not be checked")})"
|
|
4171
|
+
)
|
|
3385
4172
|
}
|
|
3386
4173
|
}
|
|
3387
4174
|
|
|
3388
4175
|
@JavascriptInterface
|
|
3389
4176
|
fun clearAuthPersistence() {
|
|
3390
4177
|
try {
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
null
|
|
3396
|
-
)
|
|
3397
|
-
}
|
|
4178
|
+
securePrefs.edit().remove(authSessionExpiryKey).apply()
|
|
4179
|
+
evaluatePromiseJavascript(
|
|
4180
|
+
"window._craftAuthPersistResolve && window._craftAuthPersistResolve({cleared: true})"
|
|
4181
|
+
)
|
|
3398
4182
|
} catch (e: Exception) {
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
null
|
|
3403
|
-
)
|
|
3404
|
-
}
|
|
4183
|
+
evaluatePromiseJavascript(
|
|
4184
|
+
"window._craftAuthPersistReject && window._craftAuthPersistReject(${jsQuote(e.message ?: "Auth persistence could not be cleared")})"
|
|
4185
|
+
)
|
|
3405
4186
|
}
|
|
3406
4187
|
}
|
|
3407
4188
|
|
|
@@ -3412,58 +4193,60 @@ class CraftBridge(
|
|
|
3412
4193
|
|
|
3413
4194
|
@JavascriptInterface
|
|
3414
4195
|
fun startAR(optionsJson: String) {
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
null
|
|
3419
|
-
)
|
|
3420
|
-
}
|
|
4196
|
+
evaluatePromiseJavascript(
|
|
4197
|
+
"window._craftARReject && window._craftARReject('ARCore requires native Activity integration. Use Sceneform or AR Fragment for full AR support.')"
|
|
4198
|
+
)
|
|
3421
4199
|
}
|
|
3422
4200
|
|
|
3423
4201
|
@JavascriptInterface
|
|
3424
4202
|
fun stopAR() {
|
|
3425
|
-
|
|
3426
|
-
webView.evaluateJavascript(
|
|
3427
|
-
"window._craftARResolve && window._craftARResolve({stopped: true})",
|
|
3428
|
-
null
|
|
3429
|
-
)
|
|
3430
|
-
}
|
|
4203
|
+
evaluatePromiseJavascript("window._craftARResolve && window._craftARResolve({stopped: true})")
|
|
3431
4204
|
}
|
|
3432
4205
|
|
|
3433
4206
|
@JavascriptInterface
|
|
3434
4207
|
fun placeARObject(model: String, positionJson: String) {
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
null
|
|
3439
|
-
)
|
|
3440
|
-
}
|
|
4208
|
+
evaluatePromiseJavascript(
|
|
4209
|
+
"window._craftARReject && window._craftARReject('ARCore requires native Activity integration')"
|
|
4210
|
+
)
|
|
3441
4211
|
}
|
|
3442
4212
|
|
|
3443
4213
|
@JavascriptInterface
|
|
3444
4214
|
fun removeARObject(objectId: String) {
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
null
|
|
3449
|
-
)
|
|
3450
|
-
}
|
|
4215
|
+
evaluatePromiseJavascript(
|
|
4216
|
+
"window._craftARReject && window._craftARReject('ARCore requires native Activity integration')"
|
|
4217
|
+
)
|
|
3451
4218
|
}
|
|
3452
4219
|
|
|
3453
4220
|
@JavascriptInterface
|
|
3454
4221
|
fun getARPlanes() {
|
|
3455
|
-
|
|
3456
|
-
webView.evaluateJavascript(
|
|
3457
|
-
"window._craftARResolve && window._craftARResolve([])",
|
|
3458
|
-
null
|
|
3459
|
-
)
|
|
3460
|
-
}
|
|
4222
|
+
evaluatePromiseJavascript("window._craftARResolve && window._craftARResolve([])")
|
|
3461
4223
|
}
|
|
3462
4224
|
|
|
3463
4225
|
// ==================== ML Kit ====================
|
|
3464
4226
|
|
|
4227
|
+
private fun createMlSettlement(): Pair<(JSONArray) -> Unit, (String) -> Unit> {
|
|
4228
|
+
val settled = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
4229
|
+
val resolve: (JSONArray) -> Unit = { results ->
|
|
4230
|
+
if (!closed && settled.compareAndSet(false, true)) {
|
|
4231
|
+
evaluatePromiseJavascript("window._craftMLResolve && window._craftMLResolve($results)")
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
val reject: (String) -> Unit = { message ->
|
|
4235
|
+
if (!closed && settled.compareAndSet(false, true)) {
|
|
4236
|
+
evaluatePromiseJavascript(
|
|
4237
|
+
"window._craftMLReject && window._craftMLReject(${jsQuote(message)})"
|
|
4238
|
+
)
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
return resolve to reject
|
|
4242
|
+
}
|
|
4243
|
+
|
|
3465
4244
|
@JavascriptInterface
|
|
3466
4245
|
fun classifyImage(imageBase64: String) {
|
|
4246
|
+
if (CraftNative.classifyImage(imageBase64)) return
|
|
4247
|
+
|
|
4248
|
+
val (resolve, reject) = createMlSettlement()
|
|
4249
|
+
|
|
3467
4250
|
try {
|
|
3468
4251
|
val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
|
|
3469
4252
|
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
@@ -3472,41 +4255,33 @@ class CraftBridge(
|
|
|
3472
4255
|
val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)
|
|
3473
4256
|
labeler.process(inputImage)
|
|
3474
4257
|
.addOnSuccessListener { labels ->
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
)
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
.addOnFailureListener { e ->
|
|
3491
|
-
activity.runOnUiThread {
|
|
3492
|
-
webView.evaluateJavascript(
|
|
3493
|
-
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3494
|
-
null
|
|
3495
|
-
)
|
|
4258
|
+
try {
|
|
4259
|
+
val results = JSONArray()
|
|
4260
|
+
for (label in labels) {
|
|
4261
|
+
val obj = JSONObject()
|
|
4262
|
+
obj.put("label", label.text)
|
|
4263
|
+
obj.put("confidence", label.confidence)
|
|
4264
|
+
obj.put("index", label.index)
|
|
4265
|
+
results.put(obj)
|
|
4266
|
+
}
|
|
4267
|
+
resolve(results)
|
|
4268
|
+
} catch (error: Exception) {
|
|
4269
|
+
reject(error.message ?: "Image classification failed")
|
|
3496
4270
|
}
|
|
3497
4271
|
}
|
|
4272
|
+
.addOnFailureListener { error -> reject(error.message ?: "Image classification failed") }
|
|
4273
|
+
.addOnCompleteListener { labeler.close() }
|
|
3498
4274
|
} catch (e: Exception) {
|
|
3499
|
-
|
|
3500
|
-
webView.evaluateJavascript(
|
|
3501
|
-
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3502
|
-
null
|
|
3503
|
-
)
|
|
3504
|
-
}
|
|
4275
|
+
reject(e.message ?: "Image classification failed")
|
|
3505
4276
|
}
|
|
3506
4277
|
}
|
|
3507
4278
|
|
|
3508
4279
|
@JavascriptInterface
|
|
3509
4280
|
fun detectObjects(imageBase64: String) {
|
|
4281
|
+
if (CraftNative.detectObjects(imageBase64)) return
|
|
4282
|
+
|
|
4283
|
+
val (resolve, reject) = createMlSettlement()
|
|
4284
|
+
|
|
3510
4285
|
try {
|
|
3511
4286
|
val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
|
|
3512
4287
|
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
@@ -3521,59 +4296,51 @@ class CraftBridge(
|
|
|
3521
4296
|
val objectDetector = ObjectDetection.getClient(options)
|
|
3522
4297
|
objectDetector.process(inputImage)
|
|
3523
4298
|
.addOnSuccessListener { detectedObjects ->
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
4299
|
+
try {
|
|
4300
|
+
val results = JSONArray()
|
|
4301
|
+
for (obj in detectedObjects) {
|
|
4302
|
+
val objJson = JSONObject()
|
|
4303
|
+
|
|
4304
|
+
// Bounding box
|
|
4305
|
+
val bbox = JSONObject()
|
|
4306
|
+
bbox.put("x", obj.boundingBox.left)
|
|
4307
|
+
bbox.put("y", obj.boundingBox.top)
|
|
4308
|
+
bbox.put("width", obj.boundingBox.width())
|
|
4309
|
+
bbox.put("height", obj.boundingBox.height())
|
|
4310
|
+
objJson.put("boundingBox", bbox)
|
|
4311
|
+
|
|
4312
|
+
// Labels
|
|
4313
|
+
val labels = JSONArray()
|
|
4314
|
+
for (label in obj.labels) {
|
|
4315
|
+
val labelJson = JSONObject()
|
|
4316
|
+
labelJson.put("label", label.text)
|
|
4317
|
+
labelJson.put("confidence", label.confidence)
|
|
4318
|
+
labelJson.put("index", label.index)
|
|
4319
|
+
labels.put(labelJson)
|
|
4320
|
+
}
|
|
4321
|
+
objJson.put("labels", labels)
|
|
4322
|
+
objJson.put("trackingId", obj.trackingId)
|
|
3547
4323
|
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
null
|
|
3554
|
-
)
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
.addOnFailureListener { e ->
|
|
3558
|
-
activity.runOnUiThread {
|
|
3559
|
-
webView.evaluateJavascript(
|
|
3560
|
-
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3561
|
-
null
|
|
3562
|
-
)
|
|
4324
|
+
results.put(objJson)
|
|
4325
|
+
}
|
|
4326
|
+
resolve(results)
|
|
4327
|
+
} catch (error: Exception) {
|
|
4328
|
+
reject(error.message ?: "Object detection failed")
|
|
3563
4329
|
}
|
|
3564
4330
|
}
|
|
4331
|
+
.addOnFailureListener { error -> reject(error.message ?: "Object detection failed") }
|
|
4332
|
+
.addOnCompleteListener { objectDetector.close() }
|
|
3565
4333
|
} catch (e: Exception) {
|
|
3566
|
-
|
|
3567
|
-
webView.evaluateJavascript(
|
|
3568
|
-
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3569
|
-
null
|
|
3570
|
-
)
|
|
3571
|
-
}
|
|
4334
|
+
reject(e.message ?: "Object detection failed")
|
|
3572
4335
|
}
|
|
3573
4336
|
}
|
|
3574
4337
|
|
|
3575
4338
|
@JavascriptInterface
|
|
3576
4339
|
fun recognizeText(imageBase64: String) {
|
|
4340
|
+
if (CraftNative.recognizeText(imageBase64)) return
|
|
4341
|
+
|
|
4342
|
+
val (resolve, reject) = createMlSettlement()
|
|
4343
|
+
|
|
3577
4344
|
try {
|
|
3578
4345
|
val imageBytes = Base64.decode(imageBase64, Base64.DEFAULT)
|
|
3579
4346
|
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
@@ -3582,48 +4349,36 @@ class CraftBridge(
|
|
|
3582
4349
|
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
|
|
3583
4350
|
recognizer.process(inputImage)
|
|
3584
4351
|
.addOnSuccessListener { visionText ->
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
for (
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
4352
|
+
try {
|
|
4353
|
+
val results = JSONArray()
|
|
4354
|
+
for (block in visionText.textBlocks) {
|
|
4355
|
+
for (line in block.lines) {
|
|
4356
|
+
val lineJson = JSONObject()
|
|
4357
|
+
lineJson.put("text", line.text)
|
|
4358
|
+
lineJson.put("confidence", line.confidence ?: 0.0)
|
|
4359
|
+
|
|
4360
|
+
val boundingBox = line.boundingBox
|
|
4361
|
+
if (boundingBox != null) {
|
|
4362
|
+
val bbox = JSONObject()
|
|
4363
|
+
bbox.put("x", boundingBox.left)
|
|
4364
|
+
bbox.put("y", boundingBox.top)
|
|
4365
|
+
bbox.put("width", boundingBox.width())
|
|
4366
|
+
bbox.put("height", boundingBox.height())
|
|
4367
|
+
lineJson.put("boundingBox", bbox)
|
|
4368
|
+
}
|
|
3601
4369
|
|
|
3602
|
-
|
|
4370
|
+
results.put(lineJson)
|
|
4371
|
+
}
|
|
3603
4372
|
}
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
"window._craftMLResolve && window._craftMLResolve($results)",
|
|
3608
|
-
null
|
|
3609
|
-
)
|
|
3610
|
-
}
|
|
3611
|
-
}
|
|
3612
|
-
.addOnFailureListener { e ->
|
|
3613
|
-
activity.runOnUiThread {
|
|
3614
|
-
webView.evaluateJavascript(
|
|
3615
|
-
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3616
|
-
null
|
|
3617
|
-
)
|
|
4373
|
+
resolve(results)
|
|
4374
|
+
} catch (error: Exception) {
|
|
4375
|
+
reject(error.message ?: "Text recognition failed")
|
|
3618
4376
|
}
|
|
3619
4377
|
}
|
|
4378
|
+
.addOnFailureListener { error -> reject(error.message ?: "Text recognition failed") }
|
|
4379
|
+
.addOnCompleteListener { recognizer.close() }
|
|
3620
4380
|
} catch (e: Exception) {
|
|
3621
|
-
|
|
3622
|
-
webView.evaluateJavascript(
|
|
3623
|
-
"window._craftMLReject && window._craftMLReject('${e.message}')",
|
|
3624
|
-
null
|
|
3625
|
-
)
|
|
3626
|
-
}
|
|
4381
|
+
reject(e.message ?: "Text recognition failed")
|
|
3627
4382
|
}
|
|
3628
4383
|
}
|
|
3629
4384
|
|
|
@@ -3635,6 +4390,11 @@ class CraftBridge(
|
|
|
3635
4390
|
|
|
3636
4391
|
@JavascriptInterface
|
|
3637
4392
|
fun updateWidget(dataJson: String) {
|
|
4393
|
+
// Zig answers through the reply channel, so returning here is what
|
|
4394
|
+
// stops the promise being settled twice. The action is passed across
|
|
4395
|
+
// rather than rebuilt there — see CraftNative.updateWidget.
|
|
4396
|
+
if (CraftNative.updateWidget(activity, "{{PACKAGE_NAME}}.WIDGET_UPDATE", dataJson)) return
|
|
4397
|
+
|
|
3638
4398
|
try {
|
|
3639
4399
|
val data = JSONObject(dataJson)
|
|
3640
4400
|
val editor = widgetPrefs.edit()
|
|
@@ -3659,66 +4419,37 @@ class CraftBridge(
|
|
|
3659
4419
|
intent.setPackage(activity.packageName)
|
|
3660
4420
|
activity.sendBroadcast(intent)
|
|
3661
4421
|
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
null
|
|
3666
|
-
)
|
|
3667
|
-
}
|
|
4422
|
+
evaluatePromiseJavascript(
|
|
4423
|
+
"window._craftWidgetResolve && window._craftWidgetResolve({updated: true})"
|
|
4424
|
+
)
|
|
3668
4425
|
} catch (e: Exception) {
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
null
|
|
3673
|
-
)
|
|
3674
|
-
}
|
|
4426
|
+
evaluatePromiseJavascript(
|
|
4427
|
+
"window._craftWidgetReject && window._craftWidgetReject(${jsQuote(e.message ?: "Widget update failed")})"
|
|
4428
|
+
)
|
|
3675
4429
|
}
|
|
3676
4430
|
}
|
|
3677
4431
|
|
|
3678
4432
|
@JavascriptInterface
|
|
3679
4433
|
fun reloadWidgets() {
|
|
3680
|
-
|
|
3681
|
-
intent.setPackage(activity.packageName)
|
|
3682
|
-
activity.sendBroadcast(intent)
|
|
4434
|
+
if (CraftNative.reloadWidgets(activity, "{{PACKAGE_NAME}}.WIDGET_UPDATE")) return
|
|
3683
4435
|
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
)
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
|
|
3692
|
-
// ==================== Google Assistant / App Actions ====================
|
|
3693
|
-
|
|
3694
|
-
@JavascriptInterface
|
|
3695
|
-
fun registerVoiceAction(phrase: String, action: String) {
|
|
3696
|
-
// App Actions are defined in shortcuts.xml, not dynamically
|
|
3697
|
-
// This stores the action for handling incoming intents
|
|
3698
|
-
val prefs = activity.getSharedPreferences("craft_voice_actions", Context.MODE_PRIVATE)
|
|
3699
|
-
prefs.edit().putString(action, phrase).apply()
|
|
4436
|
+
try {
|
|
4437
|
+
val intent = Intent("{{PACKAGE_NAME}}.WIDGET_UPDATE")
|
|
4438
|
+
intent.setPackage(activity.packageName)
|
|
4439
|
+
activity.sendBroadcast(intent)
|
|
3700
4440
|
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
"window._craftVoiceResolve && window._craftVoiceResolve({registered: true, action: '$action', phrase: '$phrase'})",
|
|
3704
|
-
null
|
|
4441
|
+
evaluatePromiseJavascript(
|
|
4442
|
+
"window._craftWidgetResolve && window._craftWidgetResolve({reloaded: true})"
|
|
3705
4443
|
)
|
|
3706
|
-
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
@JavascriptInterface
|
|
3710
|
-
fun removeVoiceAction(action: String) {
|
|
3711
|
-
val prefs = activity.getSharedPreferences("craft_voice_actions", Context.MODE_PRIVATE)
|
|
3712
|
-
prefs.edit().remove(action).apply()
|
|
3713
|
-
|
|
3714
|
-
activity.runOnUiThread {
|
|
3715
|
-
webView.evaluateJavascript(
|
|
3716
|
-
"window._craftVoiceResolve && window._craftVoiceResolve({removed: true, action: '$action'})",
|
|
3717
|
-
null
|
|
4444
|
+
} catch (error: Exception) {
|
|
4445
|
+
evaluatePromiseJavascript(
|
|
4446
|
+
"window._craftWidgetReject && window._craftWidgetReject(${jsQuote(error.message ?: "Widget reload failed")})"
|
|
3718
4447
|
)
|
|
3719
4448
|
}
|
|
3720
4449
|
}
|
|
3721
4450
|
|
|
4451
|
+
// ==================== Google Assistant / App Actions ====================
|
|
4452
|
+
|
|
3722
4453
|
// Handle incoming voice action from Google Assistant
|
|
3723
4454
|
fun handleVoiceAction(intent: Intent?) {
|
|
3724
4455
|
intent?.let {
|
|
@@ -3726,12 +4457,9 @@ class CraftBridge(
|
|
|
3726
4457
|
val data = it.dataString
|
|
3727
4458
|
|
|
3728
4459
|
if (action != null) {
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
null
|
|
3733
|
-
)
|
|
3734
|
-
}
|
|
4460
|
+
evaluateJavascriptUnlessClosed(
|
|
4461
|
+
"window.dispatchEvent(new CustomEvent('craftVoiceAction', {detail: {action: ${jsQuote(action)}, data: ${jsQuote(data)}}}));"
|
|
4462
|
+
)
|
|
3735
4463
|
}
|
|
3736
4464
|
}
|
|
3737
4465
|
}
|
|
@@ -3742,23 +4470,17 @@ class CraftBridge(
|
|
|
3742
4470
|
fun sendToWatch(messageJson: String) {
|
|
3743
4471
|
// Note: Full Wearable API requires separate Wear OS app
|
|
3744
4472
|
// This provides placeholder API for future integration
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
null
|
|
3749
|
-
)
|
|
3750
|
-
}
|
|
4473
|
+
evaluatePromiseJavascript(
|
|
4474
|
+
"window._craftWatchReject && window._craftWatchReject('Wear OS integration requires companion app setup')"
|
|
4475
|
+
)
|
|
3751
4476
|
}
|
|
3752
4477
|
|
|
3753
4478
|
@JavascriptInterface
|
|
3754
4479
|
fun updateWatchContext(contextJson: String) {
|
|
3755
4480
|
// Note: Full Wearable API requires Data Layer API setup
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
null
|
|
3760
|
-
)
|
|
3761
|
-
}
|
|
4481
|
+
evaluatePromiseJavascript(
|
|
4482
|
+
"window._craftWatchReject && window._craftWatchReject('Wear OS integration requires companion app setup')"
|
|
4483
|
+
)
|
|
3762
4484
|
}
|
|
3763
4485
|
|
|
3764
4486
|
@JavascriptInterface
|
|
@@ -3772,11 +4494,14 @@ class CraftBridge(
|
|
|
3772
4494
|
private var initialURL: String? = null
|
|
3773
4495
|
|
|
3774
4496
|
fun setInitialURL(url: String?) {
|
|
4497
|
+
if (nativeDeepLinks && CraftNative.setInitialURL(url)) return
|
|
3775
4498
|
initialURL = url
|
|
3776
4499
|
}
|
|
3777
4500
|
|
|
3778
4501
|
@JavascriptInterface
|
|
3779
4502
|
fun getInitialURL() {
|
|
4503
|
+
if (nativeDeepLinks && CraftNative.getInitialURL()) return
|
|
4504
|
+
|
|
3780
4505
|
activity.runOnUiThread {
|
|
3781
4506
|
if (initialURL != null) {
|
|
3782
4507
|
val uri = Uri.parse(initialURL)
|
|
@@ -3793,20 +4518,20 @@ class CraftBridge(
|
|
|
3793
4518
|
}
|
|
3794
4519
|
put("queryParams", queryParams)
|
|
3795
4520
|
}
|
|
3796
|
-
|
|
3797
|
-
"window._craftDeepLinkResolve && window._craftDeepLinkResolve($json)"
|
|
3798
|
-
null
|
|
4521
|
+
evaluatePromiseJavascript(
|
|
4522
|
+
"window._craftDeepLinkResolve && window._craftDeepLinkResolve($json)"
|
|
3799
4523
|
)
|
|
3800
4524
|
} else {
|
|
3801
|
-
|
|
3802
|
-
"window._craftDeepLinkResolve && window._craftDeepLinkResolve(null)"
|
|
3803
|
-
null
|
|
4525
|
+
evaluatePromiseJavascript(
|
|
4526
|
+
"window._craftDeepLinkResolve && window._craftDeepLinkResolve(null)"
|
|
3804
4527
|
)
|
|
3805
4528
|
}
|
|
3806
4529
|
}
|
|
3807
4530
|
}
|
|
3808
4531
|
|
|
3809
4532
|
fun dispatchDeepLink(url: String) {
|
|
4533
|
+
if (nativeDeepLinks && CraftNative.dispatchDeepLink(url)) return
|
|
4534
|
+
|
|
3810
4535
|
// Store as initial URL if this is the first one
|
|
3811
4536
|
if (initialURL == null) {
|
|
3812
4537
|
initialURL = url
|
|
@@ -3827,18 +4552,17 @@ class CraftBridge(
|
|
|
3827
4552
|
put("queryParams", queryParams)
|
|
3828
4553
|
}
|
|
3829
4554
|
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
null
|
|
3834
|
-
)
|
|
3835
|
-
}
|
|
4555
|
+
evaluateJavascriptUnlessClosed(
|
|
4556
|
+
"window.dispatchEvent(new CustomEvent('craftDeepLink', {detail: $json}));"
|
|
4557
|
+
)
|
|
3836
4558
|
}
|
|
3837
4559
|
|
|
3838
4560
|
// ==================== Performance Profiling ====================
|
|
3839
4561
|
|
|
3840
4562
|
@JavascriptInterface
|
|
3841
4563
|
fun getMemoryUsage(): String {
|
|
4564
|
+
CraftNative.getMemoryUsage()?.let { return it }
|
|
4565
|
+
|
|
3842
4566
|
val runtime = Runtime.getRuntime()
|
|
3843
4567
|
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
|
3844
4568
|
val maxMemory = runtime.maxMemory()
|
|
@@ -3855,17 +4579,48 @@ class CraftBridge(
|
|
|
3855
4579
|
|
|
3856
4580
|
// ==================== Helpers ====================
|
|
3857
4581
|
|
|
3858
|
-
|
|
3859
|
-
|
|
4582
|
+
/**
|
|
4583
|
+
* Give Zig a way to reach the page.
|
|
4584
|
+
*
|
|
4585
|
+
* The lambda is this class's own `runOnUiThread`/`evaluateJavascript`
|
|
4586
|
+
* pair, which is what `sendEvent` below already does — so Zig and Kotlin
|
|
4587
|
+
* deliver through exactly one implementation rather than two that have to
|
|
4588
|
+
* agree about threading.
|
|
4589
|
+
*/
|
|
4590
|
+
private fun installNativeDeliverer() {
|
|
4591
|
+
CraftNative.setDeliverer { script ->
|
|
4592
|
+
evaluateJavascriptUnlessClosed(script)
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
private fun evaluatePromiseJavascript(script: String) {
|
|
4597
|
+
evaluateJavascriptUnlessClosed(script)
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
private fun requestPermissionsBestEffort(permissions: Array<String>, requestCode: Int) {
|
|
3860
4601
|
activity.runOnUiThread {
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
null
|
|
3864
|
-
)
|
|
4602
|
+
if (closed) return@runOnUiThread
|
|
4603
|
+
runCatching { ActivityCompat.requestPermissions(activity, permissions, requestCode) }
|
|
3865
4604
|
}
|
|
3866
4605
|
}
|
|
3867
4606
|
|
|
4607
|
+
private fun evaluateJavascriptUnlessClosed(script: String) {
|
|
4608
|
+
if (closed) return
|
|
4609
|
+
activity.runOnUiThread {
|
|
4610
|
+
if (closed) return@runOnUiThread
|
|
4611
|
+
webView.evaluateJavascript(script, null)
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
private fun sendEvent(event: String, data: Map<String, Any>) {
|
|
4616
|
+
val json = JSONObject(data).toString()
|
|
4617
|
+
evaluateJavascriptUnlessClosed(
|
|
4618
|
+
"window.dispatchEvent(new CustomEvent(${jsQuote(event)}, {detail: $json}));"
|
|
4619
|
+
)
|
|
4620
|
+
}
|
|
4621
|
+
|
|
3868
4622
|
companion object {
|
|
4623
|
+
private val MIME_TYPE = Regex("^[^\\s/]+/[^\\s/]+$")
|
|
3869
4624
|
const val REQUEST_CAMERA = 1001
|
|
3870
4625
|
const val REQUEST_GALLERY = 1002
|
|
3871
4626
|
const val REQUEST_LOCATION = 1003
|
|
@@ -3876,5 +4631,8 @@ class CraftBridge(
|
|
|
3876
4631
|
const val REQUEST_VIDEO = 1008
|
|
3877
4632
|
const val REQUEST_BLUETOOTH = 1009
|
|
3878
4633
|
const val REQUEST_PICK_CONTACT = 1010
|
|
4634
|
+
private const val PERMISSION_REQUEST_START = 2000
|
|
4635
|
+
private const val PERMISSION_REQUEST_END = 2999
|
|
4636
|
+
private const val LOCATION_REQUEST_TIMEOUT_MS = 15_000L
|
|
3879
4637
|
}
|
|
3880
4638
|
}
|