@ansight/capacitor 1.0.2-preview.6

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.
@@ -0,0 +1,1131 @@
1
+ package ai.ansight.capacitor
2
+
3
+ import ai.ansight.Ansight
4
+ import ai.ansight.runtime.AndroidToolExecutionContext
5
+ import ai.ansight.runtime.AndroidToolResult
6
+ import ai.ansight.runtime.AnsightChannel
7
+ import ai.ansight.runtime.AnsightChannels
8
+ import ai.ansight.runtime.AnsightDeveloperMode
9
+ import ai.ansight.runtime.AnsightHostAutoProbeOptions
10
+ import ai.ansight.runtime.AnsightHostConnectionOptions
11
+ import ai.ansight.runtime.AnsightLogCallback
12
+ import ai.ansight.runtime.AnsightLogLevel
13
+ import ai.ansight.runtime.AnsightLogger
14
+ import ai.ansight.runtime.AnsightOptions
15
+ import ai.ansight.runtime.AnsightRuntime
16
+ import ai.ansight.runtime.AnsightSecureStorageOptions
17
+ import ai.ansight.runtime.AnsightSessionJpegCaptureOptions
18
+ import ai.ansight.runtime.AnsightToolGuard
19
+ import ai.ansight.runtime.AnsightTouchCaptureOptions
20
+ import ai.ansight.runtime.AppLifecycleState
21
+ import ai.ansight.runtime.DefaultMemoryChannels
22
+ import ai.ansight.runtime.FunctionAndroidTool
23
+ import ai.ansight.runtime.HostConnectionCapabilities
24
+ import ai.ansight.runtime.HostConnectionActionKind
25
+ import ai.ansight.runtime.HostConnectionRequest
26
+ import ai.ansight.runtime.HostConnectionRequestKind
27
+ import ai.ansight.runtime.HostConnectionResult
28
+ import ai.ansight.runtime.HostConnectionSource
29
+ import ai.ansight.runtime.HostConnectionStatus
30
+ import ai.ansight.runtime.OperationResult
31
+ import ai.ansight.runtime.OpenSessionResult
32
+ import ai.ansight.runtime.PairingFileTransferWireProtocol
33
+ import ai.ansight.runtime.PairingOpenOptions
34
+ import ai.ansight.runtime.RecordedEvent
35
+ import ai.ansight.runtime.RecordedMetric
36
+ import ai.ansight.runtime.ToolDefinition
37
+ import ai.ansight.runtime.ToolSchema
38
+ import ai.ansight.runtime.ToolScope
39
+ import ai.ansight.runtime.ToolSecurity
40
+ import ai.ansight.runtime.ToolSecurityLevel
41
+ import ai.ansight.runtime.sendBinaryTransfer
42
+ import ai.ansight.tools.database.AndroidDatabaseRoot
43
+ import ai.ansight.tools.database.AndroidDatabaseToolsOptions
44
+ import ai.ansight.tools.database.withDatabaseTools
45
+ import ai.ansight.tools.filesystem.AndroidFileSystemRoot
46
+ import ai.ansight.tools.filesystem.AndroidFileSystemToolsOptions
47
+ import ai.ansight.tools.filesystem.withFileSystemTools
48
+ import ai.ansight.tools.preferences.AndroidPreferencesToolsOptions
49
+ import ai.ansight.tools.preferences.withPreferencesTools
50
+ import ai.ansight.tools.reflection.AndroidReflectionToolsOptions
51
+ import ai.ansight.tools.reflection.withReflectionTools
52
+ import ai.ansight.tools.securestorage.withSecureStorageTools
53
+ import ai.ansight.tools.visualtree.withVisualTreeTools
54
+ import ai.ansight.pairing.AnsightPairing
55
+ import android.app.Application
56
+ import android.os.Handler
57
+ import android.os.Looper
58
+ import android.util.Base64
59
+ import com.getcapacitor.JSObject
60
+ import com.getcapacitor.Plugin
61
+ import com.getcapacitor.PluginCall
62
+ import com.getcapacitor.PluginMethod
63
+ import com.getcapacitor.annotation.CapacitorPlugin
64
+ import org.json.JSONArray
65
+ import org.json.JSONObject
66
+ import java.util.Locale
67
+ import java.util.UUID
68
+ import java.util.concurrent.ConcurrentHashMap
69
+ import java.util.concurrent.CountDownLatch
70
+ import java.util.concurrent.Executors
71
+ import java.util.concurrent.TimeUnit
72
+
73
+ @CapacitorPlugin(name = "Ansight")
74
+ class AnsightCapacitorPlugin : Plugin() {
75
+ private data class PendingToolCall(
76
+ val context: AndroidToolExecutionContext,
77
+ val latch: CountDownLatch = CountDownLatch(1),
78
+ @Volatile var result: AndroidToolResult? = null,
79
+ )
80
+
81
+ private data class CustomToolRegistration(
82
+ val definition: ToolDefinition,
83
+ val timeoutMilliseconds: Long,
84
+ )
85
+
86
+ private val executor = Executors.newCachedThreadPool()
87
+ private val pendingToolCalls = ConcurrentHashMap<String, PendingToolCall>()
88
+ private val customToolRegistrations = ConcurrentHashMap<String, CustomToolRegistration>()
89
+ private val activeCustomToolIds = ConcurrentHashMap.newKeySet<String>()
90
+ private val mainHandler = Handler(Looper.getMainLooper())
91
+ private val logCallback = AnsightLogCallback { level, message, throwable ->
92
+ val event = JSObject()
93
+ .putValue("level", level.name.lowercase(Locale.US))
94
+ .putValue("message", message)
95
+ .putValue("platform", "android")
96
+ throwable?.message?.let { event.put("error", it) }
97
+ mainHandler.post { notifyListeners("ansightLog", event) }
98
+ }
99
+
100
+ override fun load() {
101
+ AnsightLogger.registerCallback(logCallback)
102
+ }
103
+
104
+ override fun handleOnDestroy() {
105
+ AnsightLogger.removeCallback(logCallback)
106
+ executor.shutdownNow()
107
+ super.handleOnDestroy()
108
+ }
109
+
110
+ @PluginMethod
111
+ fun initialize(call: PluginCall) = resolve(call) {
112
+ Ansight.initialize(application(), buildOptions(call.data))
113
+ installRegisteredCustomTools()
114
+ snapshot()
115
+ }
116
+
117
+ @PluginMethod
118
+ fun initializeAndActivate(call: PluginCall) = resolve(call) {
119
+ Ansight.initializeAndActivate(application(), buildOptions(call.data))
120
+ bindCurrentActivity()
121
+ installRegisteredCustomTools()
122
+ snapshot()
123
+ }
124
+
125
+ @PluginMethod
126
+ fun activate(call: PluginCall) = resolve(call) {
127
+ AnsightRuntime.activate()
128
+ bindCurrentActivity()
129
+ snapshot()
130
+ }
131
+
132
+ @PluginMethod
133
+ fun deactivate(call: PluginCall) = resolve(call) {
134
+ AnsightRuntime.deactivate()
135
+ snapshot()
136
+ }
137
+
138
+ @PluginMethod
139
+ fun clear(call: PluginCall) = resolve(call) {
140
+ AnsightRuntime.clear()
141
+ snapshot()
142
+ }
143
+
144
+ @PluginMethod
145
+ fun registerMetricChannel(call: PluginCall) = resolve(call) {
146
+ val channel = call.data.objectValue("channel")
147
+ AnsightRuntime.registerMetricChannel(
148
+ AnsightChannel(
149
+ id = channel.intValue("id", -1),
150
+ name = channel.stringValue("name") ?: "",
151
+ unit = channel.stringValue("unit"),
152
+ type = channel.stringValue("type") ?: "custom",
153
+ colorHex = channel.stringValue("colorHex"),
154
+ source = channel.stringValue("source"),
155
+ group = channel.stringValue("group"),
156
+ kind = channel.stringValue("kind"),
157
+ ),
158
+ )
159
+ snapshot()
160
+ }
161
+
162
+ @PluginMethod
163
+ fun recordMetric(call: PluginCall) = resolve(call) {
164
+ AnsightRuntime.metric(
165
+ call.data.doubleValue("value", 0.0).toLong(),
166
+ call.data.intValue("channel", AnsightChannels.Unspecified),
167
+ )
168
+ snapshot()
169
+ }
170
+
171
+ @PluginMethod
172
+ fun recordEvent(call: PluginCall) = resolve(call) {
173
+ AnsightRuntime.event(
174
+ label = call.getString("label").orEmpty(),
175
+ type = eventType(call.getString("type")),
176
+ details = call.getString("details"),
177
+ channel = call.data.intValue("channel", AnsightChannels.Unspecified),
178
+ )
179
+ snapshot()
180
+ }
181
+
182
+ @PluginMethod
183
+ fun screenViewed(call: PluginCall) = resolve(call) {
184
+ AnsightRuntime.screenViewed(
185
+ call.getString("name").orEmpty(),
186
+ call.data.objectValue("details").toStringMap(),
187
+ )
188
+ snapshot()
189
+ }
190
+
191
+ @PluginMethod
192
+ fun setAppLifecycleState(call: PluginCall) = resolve(call) {
193
+ AnsightRuntime.setAppLifecycleState(lifecycleState(call.getString("state").orEmpty()))
194
+ snapshot()
195
+ }
196
+
197
+ @PluginMethod
198
+ fun connect(call: PluginCall) = background(call) {
199
+ val pairingPayload = call.getString("pairingPayload")
200
+ val request = HostConnectionRequest(
201
+ kind = if (pairingPayload.isNullOrBlank()) HostConnectionRequestKind.Auto else HostConnectionRequestKind.Payload,
202
+ payload = pairingPayload,
203
+ clientName = call.getString("clientName"),
204
+ expectedAppId = call.getString("expectedAppId"),
205
+ hostAddressOverride = call.getString("hostAddressOverride"),
206
+ )
207
+ hostConnectionResult(AnsightRuntime.connect(request))
208
+ }
209
+
210
+ @PluginMethod
211
+ fun scanPairingQrCode(call: PluginCall) {
212
+ val currentActivity = activity
213
+ if (currentActivity == null) {
214
+ call.reject(
215
+ "QR pairing is unavailable because no Android activity is available.",
216
+ "ansight_qr_unavailable",
217
+ )
218
+ return
219
+ }
220
+
221
+ AnsightPairing.scanQrCode(
222
+ activity = currentActivity,
223
+ onPayload = { payload ->
224
+ if (payload.isNullOrBlank()) {
225
+ call.resolve(
226
+ hostConnectionResult(
227
+ HostConnectionResult.failure(
228
+ message = "QR pairing canceled.",
229
+ kind = HostConnectionActionKind.Connect,
230
+ source = HostConnectionSource.ConfigReader,
231
+ reasonCode = "pairing_canceled",
232
+ ),
233
+ ),
234
+ )
235
+ return@scanQrCode
236
+ }
237
+
238
+ executor.execute {
239
+ resolve(call) {
240
+ hostConnectionResult(
241
+ AnsightRuntime.connect(
242
+ HostConnectionRequest(
243
+ kind = HostConnectionRequestKind.QrCode,
244
+ payload = payload,
245
+ clientName = call.getString("clientName"),
246
+ expectedAppId = call.getString("expectedAppId"),
247
+ hostAddressOverride = call.getString("hostAddressOverride"),
248
+ ),
249
+ ),
250
+ )
251
+ }
252
+ }
253
+ },
254
+ onError = {
255
+ call.reject(
256
+ it.message ?: "QR pairing failed.",
257
+ "ansight_qr_error",
258
+ it as? Exception,
259
+ )
260
+ },
261
+ )
262
+ }
263
+
264
+ @PluginMethod
265
+ fun openSession(call: PluginCall) = background(call) {
266
+ openSessionResult(
267
+ AnsightRuntime.openSession(
268
+ call.getString("pairingPayload").orEmpty(),
269
+ pairingOpenOptions(call.data),
270
+ ),
271
+ )
272
+ }
273
+
274
+ @PluginMethod
275
+ fun disconnect(call: PluginCall) = background(call) {
276
+ hostConnectionResult(AnsightRuntime.disconnect())
277
+ }
278
+
279
+ @PluginMethod
280
+ fun completeSession(call: PluginCall) = background(call) {
281
+ AnsightRuntime.completeSession()
282
+ operationResult(OperationResult.success("Session completed."))
283
+ }
284
+
285
+ @PluginMethod
286
+ fun closeSession(call: PluginCall) = background(call) {
287
+ AnsightRuntime.closeSession()
288
+ operationResult(OperationResult.success("Session closed."))
289
+ }
290
+
291
+ @PluginMethod
292
+ fun savePairingConfig(call: PluginCall) = background(call) {
293
+ hostConnectionResult(
294
+ AnsightRuntime.savePairingConfig(
295
+ call.getString("pairingPayload").orEmpty(),
296
+ call.getString("expectedAppId"),
297
+ ),
298
+ )
299
+ }
300
+
301
+ @PluginMethod
302
+ fun clearSavedPairing(call: PluginCall) = resolve(call) {
303
+ hostConnectionResult(AnsightRuntime.clearSavedPairingConfig())
304
+ }
305
+
306
+ @PluginMethod
307
+ fun clearCachedSession(call: PluginCall) = resolve(call) {
308
+ operationResult(AnsightRuntime.clearCachedSession())
309
+ }
310
+
311
+ @PluginMethod
312
+ fun notifyHostConnectionConfigChanged(call: PluginCall) = resolve(call) {
313
+ hostConnectionResult(AnsightRuntime.notifyHostConnectionConfigChanged())
314
+ }
315
+
316
+ @PluginMethod
317
+ fun status(call: PluginCall) = resolve(call) { snapshot() }
318
+
319
+ @PluginMethod
320
+ fun snapshot(call: PluginCall) = resolve(call) { snapshot() }
321
+
322
+ @PluginMethod
323
+ fun hostConnectionStatus(call: PluginCall) = resolve(call) {
324
+ hostConnectionStatus(AnsightRuntime.hostConnectionStatus())
325
+ }
326
+
327
+ @PluginMethod
328
+ fun hostConnectionCapabilities(call: PluginCall) = resolve(call) {
329
+ hostConnectionCapabilities(AnsightRuntime.hostConnectionCapabilities())
330
+ }
331
+
332
+ @PluginMethod
333
+ fun currentOptions(call: PluginCall) = resolve(call) {
334
+ options(AnsightRuntime.options())
335
+ }
336
+
337
+ @PluginMethod
338
+ fun recordedMetrics(call: PluginCall) = resolve(call) {
339
+ val metrics = AnsightRuntime.recordedMetrics()
340
+ val limit = call.data.intValue("limit", 0)
341
+ JSObject().putValue(
342
+ "items",
343
+ JSONArray(metrics.takeLast(if (limit > 0) limit else metrics.size).map(::metric)),
344
+ )
345
+ }
346
+
347
+ @PluginMethod
348
+ fun recordedEvents(call: PluginCall) = resolve(call) {
349
+ val events = AnsightRuntime.recordedEvents()
350
+ val limit = call.data.intValue("limit", 0)
351
+ JSObject().putValue(
352
+ "items",
353
+ JSONArray(events.takeLast(if (limit > 0) limit else events.size).map(::event)),
354
+ )
355
+ }
356
+
357
+ @PluginMethod
358
+ fun sendClientLog(call: PluginCall) = resolve(call) {
359
+ operationResult(AnsightRuntime.sendClientLog(call.getString("line").orEmpty()))
360
+ }
361
+
362
+ @PluginMethod
363
+ fun captureBuiltInTelemetrySample(call: PluginCall) = resolve(call) {
364
+ AnsightRuntime.captureBuiltInTelemetrySample()
365
+ snapshot()
366
+ }
367
+
368
+ @PluginMethod
369
+ fun isFramesPerSecondEnabled(call: PluginCall) = resolve(call) {
370
+ JSObject().putValue("value", AnsightRuntime.isFramesPerSecondEnabled())
371
+ }
372
+
373
+ @PluginMethod
374
+ fun enableFramesPerSecond(call: PluginCall) = resolve(call) {
375
+ AnsightRuntime.enableFramesPerSecond()
376
+ snapshot()
377
+ }
378
+
379
+ @PluginMethod
380
+ fun disableFramesPerSecond(call: PluginCall) = resolve(call) {
381
+ AnsightRuntime.disableFramesPerSecond()
382
+ snapshot()
383
+ }
384
+
385
+ @PluginMethod
386
+ fun captureScreenFrame(call: PluginCall) = background(call) {
387
+ bindCurrentActivity()
388
+ operationResult(AnsightRuntime.captureScreenFrame(sessionJpegCaptureOptions(call.data)))
389
+ }
390
+
391
+ @PluginMethod
392
+ fun enableTouchCapture(call: PluginCall) = resolve(call) {
393
+ operationResult(AnsightRuntime.enableTouchCapture())
394
+ }
395
+
396
+ @PluginMethod
397
+ fun disableTouchCapture(call: PluginCall) = resolve(call) {
398
+ operationResult(AnsightRuntime.disableTouchCapture())
399
+ }
400
+
401
+ @PluginMethod
402
+ fun updateSessionProperties(call: PluginCall) = resolve(call) {
403
+ operationResult(
404
+ AnsightRuntime.updateCustomProperties(
405
+ call.data.objectValue("properties").toGroupedStringMap(),
406
+ ),
407
+ )
408
+ }
409
+
410
+ @PluginMethod
411
+ fun clearSessionProperties(call: PluginCall) = resolve(call) {
412
+ operationResult(AnsightRuntime.clearCustomProperties())
413
+ }
414
+
415
+ @PluginMethod
416
+ fun registerCustomProperty(call: PluginCall) = resolve(call) {
417
+ operationResult(
418
+ AnsightRuntime.registerCustomProperty(
419
+ call.getString("group").orEmpty(),
420
+ call.getString("key").orEmpty(),
421
+ call.getString("value").orEmpty(),
422
+ ),
423
+ )
424
+ }
425
+
426
+ @PluginMethod
427
+ fun removeCustomProperty(call: PluginCall) = resolve(call) {
428
+ operationResult(
429
+ AnsightRuntime.removeCustomProperty(
430
+ call.getString("group").orEmpty(),
431
+ call.getString("key").orEmpty(),
432
+ ),
433
+ )
434
+ }
435
+
436
+ @PluginMethod
437
+ fun registerCustomTool(call: PluginCall) = resolve(call) {
438
+ val map = call.data.objectValue("definition")
439
+ val definition = toolDefinition(map)
440
+ val registration = CustomToolRegistration(
441
+ definition,
442
+ map.intValue("timeoutMilliseconds", 30_000).toLong().coerceAtLeast(250L),
443
+ )
444
+ customToolRegistrations[definition.id] = registration
445
+ activeCustomToolIds.add(definition.id)
446
+ if (AnsightRuntime.snapshot().initialized) installCustomTool(registration)
447
+ operationResult(OperationResult.success("Tool registered."))
448
+ .putValue("id", definition.id)
449
+ }
450
+
451
+ @PluginMethod
452
+ fun unregisterCustomTool(call: PluginCall) = resolve(call) {
453
+ val id = call.getString("id").orEmpty().trim()
454
+ activeCustomToolIds.remove(id)
455
+ customToolRegistrations.remove(id)
456
+ operationResult(OperationResult.success("Tool unregistered.")).putValue("id", id)
457
+ }
458
+
459
+ @PluginMethod
460
+ fun clearRegisteredCustomTools(call: PluginCall) = resolve(call) {
461
+ activeCustomToolIds.clear()
462
+ customToolRegistrations.clear()
463
+ operationResult(OperationResult.success("JavaScript tools cleared."))
464
+ }
465
+
466
+ @PluginMethod
467
+ fun resolveToolCall(call: PluginCall) {
468
+ val requestId = call.getString("requestId").orEmpty()
469
+ val pending = pendingToolCalls[requestId]
470
+ if (pending == null) {
471
+ call.resolve(
472
+ operationResult(OperationResult.failure("Tool request is no longer pending."))
473
+ .putValue("accepted", false),
474
+ )
475
+ return
476
+ }
477
+ val result = call.data.objectValue("result")
478
+ val payload = resultPayload(result)
479
+ pending.result = if (result.booleanValue("success", true)) {
480
+ AndroidToolResult.success(payload, result.stringValue("message"))
481
+ } else {
482
+ AndroidToolResult.failure(
483
+ result.stringValue("message") ?: "JavaScript tool failed.",
484
+ result.stringValue("errorCode"),
485
+ payload,
486
+ )
487
+ }
488
+ pending.latch.countDown()
489
+ call.resolve(
490
+ operationResult(OperationResult.success("Tool result accepted."))
491
+ .putValue("accepted", true),
492
+ )
493
+ }
494
+
495
+ @PluginMethod
496
+ fun queueBinaryTransfer(call: PluginCall) = resolve(call) {
497
+ val requestId = call.getString("requestId").orEmpty().trim()
498
+ val pending = pendingToolCalls[requestId]
499
+ ?: return@resolve operationResult(
500
+ OperationResult.failure("Binary transfer requires an active JavaScript tool request."),
501
+ ).putValue("errorCode", "artifact_request_unavailable")
502
+ val transport = pending.context.transport
503
+ ?: return@resolve operationResult(
504
+ OperationResult.failure("Binary transfers require an active pairing session."),
505
+ ).putValue("errorCode", "artifact_transfer_unavailable")
506
+ val bytes = Base64.decode(call.getString("base64Data").orEmpty(), Base64.DEFAULT)
507
+ val chunkBytes = call.data.intValue("chunkBytes", 65_536).coerceIn(1_024, 512 * 1_024)
508
+ val transferId = PairingFileTransferWireProtocol.newTransferId()
509
+ executor.execute { transport.sendBinaryTransfer(transferId, bytes, chunkBytes) }
510
+ operationResult(OperationResult.success("Binary transfer queued."))
511
+ .putValue("transferId", transferId)
512
+ .putValue("deliveryMode", "websocket_binary")
513
+ .putValue("wireProtocol", PairingFileTransferWireProtocol.ProtocolName)
514
+ .putValue("status", "queued")
515
+ .putValue("chunkBytes", chunkBytes)
516
+ .putValue("sizeBytes", bytes.size)
517
+ }
518
+
519
+ private fun executeJavaScriptTool(
520
+ toolId: String,
521
+ arguments: Map<String, String>,
522
+ context: AndroidToolExecutionContext,
523
+ timeoutMilliseconds: Long,
524
+ ): AndroidToolResult {
525
+ if (toolId !in activeCustomToolIds) {
526
+ return AndroidToolResult.failure(
527
+ "Tool '$toolId' is no longer registered in JavaScript.",
528
+ "javascript_tool_unregistered",
529
+ )
530
+ }
531
+ val requestId = "android.capacitor.${UUID.randomUUID().toString().replace("-", "")}"
532
+ val pending = PendingToolCall(context)
533
+ pendingToolCalls[requestId] = pending
534
+ val event = JSObject()
535
+ .putValue("requestId", requestId)
536
+ .putValue("toolId", toolId)
537
+ .putValue("platform", "android")
538
+ .putValue("sessionId", context.sessionId)
539
+ .putValue("nativeRequestId", context.requestId)
540
+ .putValue("arguments", arguments.toJSObject())
541
+ mainHandler.post { notifyListeners("ansightToolCall", event) }
542
+ val completed = pending.latch.await(timeoutMilliseconds, TimeUnit.MILLISECONDS)
543
+ pendingToolCalls.remove(requestId)
544
+ if (!completed) {
545
+ return AndroidToolResult.failure(
546
+ "JavaScript handler for '$toolId' timed out.",
547
+ "javascript_tool_timeout",
548
+ )
549
+ }
550
+ return pending.result ?: AndroidToolResult.failure(
551
+ "JavaScript handler returned no result.",
552
+ "javascript_tool_result_missing",
553
+ )
554
+ }
555
+
556
+ private fun installRegisteredCustomTools() {
557
+ customToolRegistrations.values.forEach(::installCustomTool)
558
+ }
559
+
560
+ private fun installCustomTool(registration: CustomToolRegistration) {
561
+ activeCustomToolIds.add(registration.definition.id)
562
+ AnsightRuntime.registerTool(
563
+ FunctionAndroidTool(registration.definition) { arguments, context ->
564
+ executeJavaScriptTool(
565
+ registration.definition.id,
566
+ arguments,
567
+ context,
568
+ registration.timeoutMilliseconds,
569
+ )
570
+ },
571
+ replaceExisting = true,
572
+ )
573
+ }
574
+
575
+ private fun buildOptions(map: JSObject): AnsightOptions {
576
+ val useDefaults = map.booleanValue("useNativeAllInOneDefaults", false)
577
+ val pairingConfigJson = map.stringValue("pairingConfigJson")
578
+ var options = if (useDefaults) {
579
+ AnsightDeveloperMode.options(
580
+ bundledDeveloperConfigJson = pairingConfigJson,
581
+ clientName = map.stringValue("clientName"),
582
+ )
583
+ } else {
584
+ AnsightOptions()
585
+ }
586
+ if (!useDefaults && !pairingConfigJson.isNullOrBlank()) {
587
+ options = options.copy(
588
+ hostConnection = options.hostConnection.copy(bundledConfigJson = pairingConfigJson),
589
+ )
590
+ }
591
+ if (map.has("sampleFrequencyMilliseconds")) {
592
+ options = options.copy(
593
+ sampleFrequencyMilliseconds = map.intValue(
594
+ "sampleFrequencyMilliseconds",
595
+ options.sampleFrequencyMilliseconds,
596
+ ),
597
+ )
598
+ }
599
+ if (map.has("retentionPeriodSeconds")) {
600
+ options = options.copy(
601
+ retentionPeriodSeconds = map.intValue(
602
+ "retentionPeriodSeconds",
603
+ options.retentionPeriodSeconds,
604
+ ),
605
+ )
606
+ }
607
+ if (map.has("enableFramesPerSecond")) {
608
+ options = options.copy(
609
+ enableFramesPerSecond = map.booleanValue(
610
+ "enableFramesPerSecond",
611
+ options.enableFramesPerSecond,
612
+ ),
613
+ )
614
+ }
615
+ if (map.has("enableBatteryLevel")) {
616
+ options = options.copy(
617
+ enableBatteryLevel = map.booleanValue("enableBatteryLevel", options.enableBatteryLevel),
618
+ )
619
+ }
620
+ map.objectValueOrNull("defaultMemoryChannels")?.let { memory ->
621
+ options = options.copy(
622
+ defaultMemoryChannels = DefaultMemoryChannels(
623
+ javaHeap = memory.booleanValue(
624
+ "managedHeap",
625
+ memory.booleanValue("javaHeap", options.defaultMemoryChannels.javaHeap),
626
+ ),
627
+ nativeHeap = memory.booleanValue(
628
+ "nativeHeap",
629
+ options.defaultMemoryChannels.nativeHeap,
630
+ ),
631
+ rss = memory.booleanValue(
632
+ "residentSetSize",
633
+ memory.booleanValue("rss", options.defaultMemoryChannels.rss),
634
+ ),
635
+ ),
636
+ )
637
+ }
638
+ map.arrayValue("additionalChannels")?.let { channels ->
639
+ options = options.copy(
640
+ additionalChannels = (0 until channels.length()).mapNotNull { index ->
641
+ channels.optJSONObject(index)?.let(::channel)
642
+ },
643
+ )
644
+ }
645
+ if (map.has("sessionJpegCapture")) {
646
+ options = options.copy(
647
+ sessionJpegCapture = if (map.opt("sessionJpegCapture") == false) {
648
+ null
649
+ } else {
650
+ sessionJpegCaptureOptions(map.objectValue("sessionJpegCapture"))
651
+ },
652
+ )
653
+ }
654
+ if (map.has("touchCapture")) {
655
+ options = options.copy(
656
+ touchCapture = if (map.opt("touchCapture") == false) {
657
+ null
658
+ } else {
659
+ val touch = map.objectValue("touchCapture")
660
+ AnsightTouchCaptureOptions(
661
+ moveCaptureDistanceThreshold = touch.doubleValue(
662
+ "moveCaptureDistanceThreshold",
663
+ 8.0,
664
+ ),
665
+ moveCaptureFramesPerSecond = touch.intValue(
666
+ "moveCaptureFramesPerSecond",
667
+ 20,
668
+ ),
669
+ )
670
+ },
671
+ )
672
+ }
673
+ map.stringValue("toolGuard")?.let { options = options.copy(toolGuard = toolGuard(it)) }
674
+ map.objectValueOrNull("customProperties")?.let {
675
+ options = options.copy(customProperties = it.toGroupedStringMap())
676
+ }
677
+ map.objectValueOrNull("hostAutoProbe")?.let { autoProbe ->
678
+ options = options.copy(
679
+ hostAutoProbe = AnsightHostAutoProbeOptions(
680
+ enabled = autoProbe.booleanValue("enabled", options.hostAutoProbe.enabled),
681
+ initialDelayMilliseconds = autoProbe.longValue(
682
+ "initialDelayMilliseconds",
683
+ options.hostAutoProbe.initialDelayMilliseconds,
684
+ ),
685
+ probeIntervalMilliseconds = autoProbe.longValue(
686
+ "probeIntervalMilliseconds",
687
+ options.hostAutoProbe.probeIntervalMilliseconds,
688
+ ),
689
+ reconnectDelayMilliseconds = autoProbe.longValue(
690
+ "reconnectDelayMilliseconds",
691
+ options.hostAutoProbe.reconnectDelayMilliseconds,
692
+ ),
693
+ clientName = autoProbe.stringValue("clientName") ?: options.hostAutoProbe.clientName,
694
+ ),
695
+ )
696
+ }
697
+ map.objectValueOrNull("hostConnection")?.let { host ->
698
+ options = options.copy(
699
+ hostConnection = AnsightHostConnectionOptions(
700
+ savedConfigKey = host.stringValue("savedConfigKey")
701
+ ?: options.hostConnection.savedConfigKey,
702
+ bundledConfigJson = host.stringValue("bundledConfigJson")
703
+ ?: options.hostConnection.bundledConfigJson,
704
+ bundledDeveloperConfigJson = host.stringValue("bundledDeveloperConfigJson")
705
+ ?: options.hostConnection.bundledDeveloperConfigJson,
706
+ discoveryPort = host.optionalInt("discoveryPort")
707
+ ?: options.hostConnection.discoveryPort,
708
+ connectionProfileRetentionSeconds = host.longValue(
709
+ "connectionProfileRetentionSeconds",
710
+ options.hostConnection.connectionProfileRetentionSeconds,
711
+ ),
712
+ ),
713
+ )
714
+ }
715
+ map.objectValueOrNull("secureStorage")?.let { secure ->
716
+ options = options.copy(
717
+ secureStorage = AnsightSecureStorageOptions(
718
+ preferencesName = secure.stringValue("preferencesName")
719
+ ?: options.secureStorage.preferencesName,
720
+ allowedKeys = secure.stringSet("allowedKeys"),
721
+ allowedPrefixes = secure.stringSet("allowedPrefixes"),
722
+ ),
723
+ )
724
+ }
725
+ return options.withNativeTools(map, useDefaults)
726
+ }
727
+
728
+ private fun AnsightOptions.withNativeTools(map: JSObject, enableVisualTreeByDefault: Boolean): AnsightOptions {
729
+ val remote = map.objectValue("remoteTools")
730
+ val builder = AnsightOptions.createBuilder(this)
731
+ if (remote.toolSuiteEnabled("visualTree", enableVisualTreeByDefault)) {
732
+ builder.withVisualTreeTools()
733
+ }
734
+ val database = remote.objectValue("database")
735
+ builder.withDatabaseTools(
736
+ AndroidDatabaseToolsOptions(
737
+ additionalRoots = roots(database.arrayValue("additionalRoots")).map {
738
+ AndroidDatabaseRoot(it.first, it.second)
739
+ },
740
+ includePlatformRoots = database.booleanValue("includePlatformRoots", true),
741
+ ).validated(),
742
+ )
743
+ val files = remote.objectValue("fileSystem")
744
+ builder.withFileSystemTools(
745
+ AndroidFileSystemToolsOptions(
746
+ additionalRoots = roots(files.arrayValue("additionalRoots")).map {
747
+ AndroidFileSystemRoot(it.first, it.second)
748
+ },
749
+ ).validated(),
750
+ )
751
+ val preferences = remote.objectValue("preferences")
752
+ builder.withPreferencesTools(
753
+ AndroidPreferencesToolsOptions(
754
+ defaultStore = preferences.stringValue("defaultStore"),
755
+ allowedStores = preferences.stringSet("allowedStores"),
756
+ allowedKeys = preferences.stringSet("allowedKeys"),
757
+ allowedKeyPrefixes = preferences.stringSet("allowedKeyPrefixes"),
758
+ ).validated(),
759
+ )
760
+ val reflection = remote.objectValue("reflection")
761
+ builder.withReflectionTools(
762
+ AndroidReflectionToolsOptions(
763
+ includeBuiltInRoots = reflection.booleanValue("includeBuiltInRoots", true),
764
+ allowedRootIds = reflection.stringSet("allowedRootIds"),
765
+ allowedTypePrefixes = reflection.stringSet("allowedTypePrefixes"),
766
+ ).validated(),
767
+ )
768
+ val secure = remote.objectValueOrNull("secureStorage") ?: map.objectValue("secureStorage")
769
+ builder.withSecureStorageTools(
770
+ AnsightSecureStorageOptions(
771
+ preferencesName = secure.stringValue("preferencesName") ?: secureStorage.preferencesName,
772
+ allowedKeys = secure.stringSet("allowedKeys"),
773
+ allowedPrefixes = secure.stringSet("allowedKeyPrefixes") + secure.stringSet("allowedPrefixes"),
774
+ ).validated(),
775
+ )
776
+ return builder.build()
777
+ }
778
+
779
+ private fun toolDefinition(map: JSObject): ToolDefinition =
780
+ ToolDefinition(
781
+ id = map.stringValue("id") ?: "",
782
+ name = map.stringValue("name") ?: map.stringValue("id") ?: "",
783
+ description = map.stringValue("description") ?: "",
784
+ category = map.stringValue("category") ?: "custom",
785
+ scope = toolScope(map.stringValue("scope")),
786
+ keywords = map.stringValue("keywords")
787
+ ?: map.stringList("keywords").joinToString(" ").ifBlank { "capacitor javascript custom tool" },
788
+ argumentsSchema = schema(map.objectValueOrNull("argumentsSchema")),
789
+ resultSchema = schema(map.objectValueOrNull("resultSchema")),
790
+ security = toolSecurity(map.objectValueOrNull("security")),
791
+ ).validated()
792
+
793
+ private fun schema(map: JSObject?): ToolSchema {
794
+ if (map == null) return ToolSchema.obj(additionalProperties = true)
795
+ val properties = mutableMapOf<String, ToolSchema>()
796
+ map.objectValueOrNull("properties")?.let { values ->
797
+ values.keys().forEach { key ->
798
+ values.objectValueOrNull(key)?.let { properties[key] = schema(it) }
799
+ }
800
+ }
801
+ return ToolSchema(
802
+ type = map.stringValue("type") ?: map.stringList("type").firstOrNull { it != "null" } ?: "object",
803
+ description = map.stringValue("description"),
804
+ properties = properties,
805
+ required = map.stringList("required"),
806
+ items = map.objectValueOrNull("items")?.let(::schema),
807
+ enumValues = map.stringList("enum"),
808
+ additionalProperties = map.booleanValue("additionalProperties", false),
809
+ nullable = "null" in map.stringList("type"),
810
+ format = map.stringValue("format"),
811
+ )
812
+ }
813
+
814
+ private fun toolSecurity(map: JSObject?): ToolSecurity {
815
+ if (map == null) return ToolSecurity.Unspecified
816
+ return ToolSecurity(
817
+ level = when (map.stringValue("level")?.lowercase()) {
818
+ "medium", "moderate" -> ToolSecurityLevel.Medium
819
+ "high" -> ToolSecurityLevel.High
820
+ "critical" -> ToolSecurityLevel.Critical
821
+ else -> ToolSecurityLevel.Low
822
+ },
823
+ implications = map.stringList("implications"),
824
+ )
825
+ }
826
+
827
+ private fun resultPayload(map: JSObject): JSONObject? {
828
+ if (!map.has("result") || map.isNull("result")) return null
829
+ return when (val value = map.opt("result")) {
830
+ is JSONObject -> value
831
+ is JSONArray -> JSONObject().put("value", value)
832
+ else -> JSONObject().put("value", value)
833
+ }
834
+ }
835
+
836
+ private fun snapshot(): JSObject {
837
+ val value = AnsightRuntime.snapshot()
838
+ return JSObject()
839
+ .putValue("initialized", value.initialized)
840
+ .putValue("active", value.active)
841
+ .putValue("sessionOpen", value.sessionOpen)
842
+ .putValue("lifecycleState", value.lifecycleState.wireName)
843
+ .putValue("lifecycleChangedAtUtc", value.lifecycleChangedAtUtc)
844
+ .putValue("metricsRecorded", value.metricsRecorded)
845
+ .putValue("eventsRecorded", value.eventsRecorded)
846
+ .putValue("touchesRecorded", value.touchesRecorded)
847
+ .putValue("registeredTools", value.registeredTools)
848
+ .putValue("sessionMessage", value.sessionMessage)
849
+ .putValue("connectionStatus", hostConnectionStatus(value.connectionStatus))
850
+ .putValue("channels", JSONArray(value.channels.map(::channel)))
851
+ .apply {
852
+ value.lastMetric?.let { put("lastMetric", metric(it)) }
853
+ value.lastEvent?.let { put("lastEvent", event(it)) }
854
+ value.currentScreen?.let {
855
+ put(
856
+ "currentScreen",
857
+ JSObject()
858
+ .putValue("name", it.name)
859
+ .putValue("capturedAtUtc", it.capturedAtUtc)
860
+ .putValue("details", it.details.toJSObject()),
861
+ )
862
+ }
863
+ }
864
+ }
865
+
866
+ private fun hostConnectionStatus(value: HostConnectionStatus): JSObject =
867
+ JSObject()
868
+ .putValue("isRuntimeActive", value.isRuntimeActive)
869
+ .putValue("isConnected", value.isConnected)
870
+ .putValue("connectionState", value.connectionState.name)
871
+ .putValue("hasCachedSession", value.hasCachedSession)
872
+ .putValue("hasSavedConfig", value.hasSavedConfig)
873
+ .putValue("hasBundledConfig", value.hasBundledConfig)
874
+ .putValue("summaryKind", value.summaryKind.name)
875
+ .putValue("summaryMessage", value.summaryMessage)
876
+
877
+ private fun hostConnectionCapabilities(value: HostConnectionCapabilities): JSObject =
878
+ JSObject()
879
+ .putValue("canConnectUsingSavedConfig", value.canConnectUsingSavedConfig)
880
+ .putValue("canConnectUsingBundledConfig", value.canConnectUsingBundledConfig)
881
+ .putValue("canChooseConfigFile", value.canChooseConfigFile)
882
+ .putValue("canScanConfigQrCode", value.canScanConfigQrCode)
883
+ .putValue("canClearSavedConfigs", value.canClearSavedConfigs)
884
+
885
+ private fun openSessionResult(value: OpenSessionResult): JSObject =
886
+ operationResult(OperationResult(value.success, value.message))
887
+ .putValue("accepted", value.accepted)
888
+ .putValue("sessionId", value.sessionId)
889
+ .putValue("configId", value.configId)
890
+ .putValue("appId", value.appId)
891
+ .putValue("resolvedHostAddress", value.resolvedHostAddress)
892
+ .putValue("usedEmbeddedDeveloperPairing", value.usedEmbeddedDeveloperPairing)
893
+ .putValue("discoverySource", value.discoverySource)
894
+ .putValue("reasonCode", value.reasonCode)
895
+ .putValue("hostId", value.hostId)
896
+ .putValue("hostName", value.hostName)
897
+
898
+ private fun hostConnectionResult(value: HostConnectionResult): JSObject =
899
+ JSObject()
900
+ .putValue("success", value.success)
901
+ .putValue("message", value.message)
902
+ .putValue("kind", value.kind.name)
903
+ .putValue("source", value.source.name)
904
+ .putValue("reasonCode", value.reasonCode ?: value.openSession?.reasonCode)
905
+ .apply {
906
+ value.openSession?.let {
907
+ put("sessionId", it.sessionId)
908
+ put("configId", it.configId)
909
+ put("appId", it.appId)
910
+ put("resolvedHostAddress", it.resolvedHostAddress)
911
+ put("hostId", it.hostId)
912
+ put("hostName", it.hostName)
913
+ put("accepted", it.accepted)
914
+ put("usedEmbeddedDeveloperPairing", it.usedEmbeddedDeveloperPairing)
915
+ put("discoverySource", it.discoverySource)
916
+ }
917
+ }
918
+
919
+ private fun operationResult(value: OperationResult): JSObject =
920
+ JSObject().putValue("success", value.success).putValue("message", value.message)
921
+
922
+ private fun options(value: AnsightOptions): JSObject =
923
+ JSObject()
924
+ .putValue("sampleFrequencyMilliseconds", value.sampleFrequencyMilliseconds)
925
+ .putValue("retentionPeriodSeconds", value.retentionPeriodSeconds)
926
+ .putValue("enableFramesPerSecond", value.enableFramesPerSecond)
927
+ .putValue("enableBatteryLevel", value.enableBatteryLevel)
928
+ .putValue("toolGuard", toolGuardName(value.toolGuard))
929
+ .putValue("customProperties", value.customProperties.toGroupedJSObject())
930
+ .putValue("additionalChannels", JSONArray(value.additionalChannels.map(::channel)))
931
+
932
+ private fun channel(value: JSONObject): AnsightChannel =
933
+ AnsightChannel(
934
+ id = value.optInt("id", -1),
935
+ name = value.optString("name"),
936
+ unit = value.stringValue("unit"),
937
+ type = value.stringValue("type") ?: "custom",
938
+ colorHex = value.stringValue("colorHex"),
939
+ source = value.stringValue("source"),
940
+ group = value.stringValue("group"),
941
+ kind = value.stringValue("kind"),
942
+ )
943
+
944
+ private fun channel(value: AnsightChannel): JSObject =
945
+ JSObject()
946
+ .putValue("id", value.id)
947
+ .putValue("name", value.name)
948
+ .putValue("unit", value.unit)
949
+ .putValue("type", value.type)
950
+ .putValue("colorHex", value.colorHex)
951
+ .putValue("source", value.source)
952
+ .putValue("group", value.group)
953
+ .putValue("kind", value.kind)
954
+
955
+ private fun metric(value: RecordedMetric): JSObject =
956
+ JSObject()
957
+ .putValue("value", value.value)
958
+ .putValue("capturedAtUtc", value.capturedAtUtc)
959
+ .putValue("capturedAtEpochMs", value.capturedAtEpochMs)
960
+ .putValue("channel", value.channel)
961
+ .putValue("sequence", value.sequence)
962
+
963
+ private fun event(value: RecordedEvent): JSObject =
964
+ JSObject()
965
+ .putValue("id", value.id)
966
+ .putValue("label", value.label)
967
+ .putValue("type", value.type.wireName)
968
+ .putValue("details", value.details)
969
+ .putValue("capturedAtUtc", value.capturedAtUtc)
970
+ .putValue("capturedAtEpochMs", value.capturedAtEpochMs)
971
+ .putValue("externalId", value.externalId)
972
+ .putValue("channel", value.channel)
973
+ .putValue("sequence", value.sequence)
974
+
975
+ private fun pairingOpenOptions(map: JSObject): PairingOpenOptions =
976
+ PairingOpenOptions(
977
+ clientName = map.stringValue("clientName") ?: "Capacitor",
978
+ expectedAppId = map.stringValue("expectedAppId"),
979
+ hostAddressOverride = map.stringValue("hostAddressOverride"),
980
+ )
981
+
982
+ private fun sessionJpegCaptureOptions(map: JSObject): AnsightSessionJpegCaptureOptions =
983
+ AnsightSessionJpegCaptureOptions(
984
+ intervalMilliseconds = map.intValue(
985
+ "intervalMilliseconds",
986
+ AnsightSessionJpegCaptureOptions.DefaultIntervalMilliseconds,
987
+ ),
988
+ quality = map.intValue("quality", AnsightSessionJpegCaptureOptions.DefaultQuality),
989
+ maxWidth = map.optionalInt("maxWidth") ?: AnsightSessionJpegCaptureOptions.DefaultMaxWidth,
990
+ captureGpuBackedSurfaces = map.booleanValue(
991
+ "captureGpuBackedSurfaces",
992
+ AnsightSessionJpegCaptureOptions.DefaultCaptureGpuBackedSurfaces,
993
+ ),
994
+ )
995
+
996
+ private fun application(): Application =
997
+ context.applicationContext as? Application
998
+ ?: error("Capacitor application context is not an Android Application.")
999
+
1000
+ private fun bindCurrentActivity() {
1001
+ activity?.let { AnsightRuntime.bindActivity(it) }
1002
+ }
1003
+
1004
+ private fun resolve(call: PluginCall, block: () -> JSObject) {
1005
+ runCatching(block).fold(
1006
+ onSuccess = call::resolve,
1007
+ onFailure = { call.reject(it.message ?: "Ansight operation failed.", "ansight_error", it as? Exception) },
1008
+ )
1009
+ }
1010
+
1011
+ private fun background(call: PluginCall, block: () -> JSObject) {
1012
+ executor.execute { resolve(call, block) }
1013
+ }
1014
+ }
1015
+
1016
+ private fun JSObject.putValue(key: String, value: Any?): JSObject = apply {
1017
+ if (value == null) put(key, JSONObject.NULL) else put(key, value)
1018
+ }
1019
+
1020
+ private fun JSObject.stringValue(key: String): String? =
1021
+ optString(key).takeIf { has(key) && !isNull(key) && it.isNotBlank() }
1022
+
1023
+ private fun JSONObject.stringValue(key: String): String? =
1024
+ optString(key).takeIf { has(key) && !isNull(key) && it.isNotBlank() }
1025
+
1026
+ private fun JSObject.booleanValue(key: String, default: Boolean): Boolean =
1027
+ if (has(key) && !isNull(key)) optBoolean(key, default) else default
1028
+
1029
+ private fun JSObject.intValue(key: String, default: Int): Int =
1030
+ if (has(key) && !isNull(key)) optInt(key, default) else default
1031
+
1032
+ private fun JSObject.longValue(key: String, default: Long): Long =
1033
+ if (has(key) && !isNull(key)) optLong(key, default) else default
1034
+
1035
+ private fun JSObject.doubleValue(key: String, default: Double): Double =
1036
+ if (has(key) && !isNull(key)) optDouble(key, default) else default
1037
+
1038
+ private fun JSObject.optionalInt(key: String): Int? =
1039
+ if (has(key) && !isNull(key)) optInt(key) else null
1040
+
1041
+ private fun JSObject.objectValue(key: String): JSObject =
1042
+ objectValueOrNull(key) ?: JSObject()
1043
+
1044
+ private fun JSObject.objectValueOrNull(key: String): JSObject? {
1045
+ val value = optJSONObject(key) ?: return null
1046
+ return if (value is JSObject) value else JSObject(value.toString())
1047
+ }
1048
+
1049
+ private fun JSObject.arrayValue(key: String): JSONArray? = optJSONArray(key)
1050
+
1051
+ private fun JSObject.stringList(key: String): List<String> {
1052
+ val array = optJSONArray(key) ?: return emptyList()
1053
+ return (0 until array.length()).mapNotNull { array.optString(it).takeIf(String::isNotBlank) }
1054
+ }
1055
+
1056
+ private fun JSObject.stringSet(key: String): Set<String> = stringList(key).toSet()
1057
+
1058
+ private fun JSObject.toolSuiteEnabled(key: String, default: Boolean): Boolean =
1059
+ when (val value = opt(key)) {
1060
+ is Boolean -> value
1061
+ is JSONObject -> value.optBoolean("enabled", true)
1062
+ else -> default
1063
+ }
1064
+
1065
+ private fun JSObject.toStringMap(): Map<String, String> =
1066
+ keys().asSequence().associateWith { key -> opt(key)?.takeUnless { it == JSONObject.NULL }?.toString().orEmpty() }
1067
+
1068
+ private fun JSObject.toGroupedStringMap(): Map<String, Map<String, String>> =
1069
+ keys().asSequence().mapNotNull { key ->
1070
+ objectValueOrNull(key)?.let { key to it.toStringMap() }
1071
+ }.toMap()
1072
+
1073
+ private fun Map<String, *>.toJSObject(): JSObject =
1074
+ JSObject().also { result -> forEach { (key, value) -> result.putValue(key, value) } }
1075
+
1076
+ private fun Map<String, Map<String, String>>.toGroupedJSObject(): JSObject =
1077
+ JSObject().also { result -> forEach { (key, value) -> result.put(key, value.toJSObject()) } }
1078
+
1079
+ private fun roots(array: JSONArray?): List<Pair<String, String>> {
1080
+ if (array == null) return emptyList()
1081
+ return (0 until array.length()).mapNotNull { index ->
1082
+ val value = array.optJSONObject(index) ?: return@mapNotNull null
1083
+ val alias = value.stringValue("alias") ?: return@mapNotNull null
1084
+ val path = value.stringValue("path") ?: return@mapNotNull null
1085
+ alias to path
1086
+ }
1087
+ }
1088
+
1089
+ private fun eventType(raw: String?): ai.ansight.runtime.AnsightEventType =
1090
+ when (raw?.trim()?.lowercase()) {
1091
+ "event" -> ai.ansight.runtime.AnsightEventType.Event
1092
+ "debug" -> ai.ansight.runtime.AnsightEventType.Debug
1093
+ "warning", "warn" -> ai.ansight.runtime.AnsightEventType.Warning
1094
+ "error" -> ai.ansight.runtime.AnsightEventType.Error
1095
+ "exception" -> ai.ansight.runtime.AnsightEventType.Exception
1096
+ "gc" -> ai.ansight.runtime.AnsightEventType.Gc
1097
+ "navigation" -> ai.ansight.runtime.AnsightEventType.Navigation
1098
+ "screenviewed", "screen_viewed" -> ai.ansight.runtime.AnsightEventType.ScreenViewed
1099
+ "lifecycle" -> ai.ansight.runtime.AnsightEventType.Lifecycle
1100
+ else -> ai.ansight.runtime.AnsightEventType.Info
1101
+ }
1102
+
1103
+ private fun lifecycleState(raw: String): AppLifecycleState =
1104
+ when (raw.trim().lowercase()) {
1105
+ "foreground", "active" -> AppLifecycleState.Foreground
1106
+ "background", "inactive" -> AppLifecycleState.Background
1107
+ else -> AppLifecycleState.Unknown
1108
+ }
1109
+
1110
+ private fun toolGuard(raw: String): AnsightToolGuard =
1111
+ when (raw.trim().lowercase()) {
1112
+ "readonly", "read_only", "read" -> AnsightToolGuard.ReadOnly
1113
+ "readwrite", "read_write", "write" -> AnsightToolGuard.ReadWrite
1114
+ "full", "fullaccess", "full_access" -> AnsightToolGuard.FullAccess
1115
+ else -> AnsightToolGuard.Disabled
1116
+ }
1117
+
1118
+ private fun toolGuardName(value: AnsightToolGuard): String =
1119
+ when (value) {
1120
+ AnsightToolGuard.Disabled -> "disabled"
1121
+ AnsightToolGuard.ReadOnly -> "readOnly"
1122
+ AnsightToolGuard.ReadWrite -> "readWrite"
1123
+ AnsightToolGuard.FullAccess -> "fullAccess"
1124
+ }
1125
+
1126
+ private fun toolScope(raw: String?): ToolScope =
1127
+ when (raw?.trim()?.lowercase()) {
1128
+ "write" -> ToolScope.Write
1129
+ "delete" -> ToolScope.Delete
1130
+ else -> ToolScope.Read
1131
+ }