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