@ansight/react-native 1.0.2-preview.1

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,1353 @@
1
+ package ai.ansight.reactnative
2
+
3
+ import ai.ansight.Ansight
4
+ import ai.ansight.runtime.AnsightDeveloperMode
5
+ import ai.ansight.runtime.AndroidToolExecutionContext
6
+ import ai.ansight.runtime.AndroidToolResult
7
+ import ai.ansight.runtime.AnsightChannel
8
+ import ai.ansight.runtime.AnsightChannels
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.AnsightOptionsBuilder
16
+ import ai.ansight.runtime.AnsightRuntime
17
+ import ai.ansight.runtime.AnsightSecureStorageOptions
18
+ import ai.ansight.runtime.AnsightSessionJpegCaptureOptions
19
+ import ai.ansight.runtime.AnsightToolGuard
20
+ import ai.ansight.runtime.AnsightTouchCaptureOptions
21
+ import ai.ansight.runtime.AppLifecycleState
22
+ import ai.ansight.runtime.DefaultMemoryChannels
23
+ import ai.ansight.runtime.FunctionAndroidTool
24
+ import ai.ansight.runtime.HostConnectionRequest
25
+ import ai.ansight.runtime.HostConnectionRequestKind
26
+ import ai.ansight.runtime.HostConnectionCapabilities
27
+ import ai.ansight.runtime.HostConnectionResult
28
+ import ai.ansight.runtime.HostConnectionStatus
29
+ import ai.ansight.runtime.OperationResult
30
+ import ai.ansight.runtime.OpenSessionResult
31
+ import ai.ansight.runtime.PairingOpenOptions
32
+ import ai.ansight.runtime.PairingFileTransferWireProtocol
33
+ import ai.ansight.runtime.RecordedEvent
34
+ import ai.ansight.runtime.RecordedMetric
35
+ import ai.ansight.runtime.ToolDefinition
36
+ import ai.ansight.runtime.ToolSchema
37
+ import ai.ansight.runtime.ToolScope
38
+ import ai.ansight.runtime.ToolSecurity
39
+ import ai.ansight.runtime.ToolSecurityLevel
40
+ import ai.ansight.runtime.sendBinaryTransfer
41
+ import ai.ansight.tools.database.AndroidDatabaseRoot
42
+ import ai.ansight.tools.database.AndroidDatabaseToolsOptions
43
+ import ai.ansight.tools.database.withDatabaseTools
44
+ import ai.ansight.tools.filesystem.AndroidFileSystemRoot
45
+ import ai.ansight.tools.filesystem.AndroidFileSystemToolsOptions
46
+ import ai.ansight.tools.filesystem.withFileSystemTools
47
+ import ai.ansight.tools.preferences.AndroidPreferencesToolsOptions
48
+ import ai.ansight.tools.preferences.withPreferencesTools
49
+ import ai.ansight.tools.reflection.AndroidReflectionToolsOptions
50
+ import ai.ansight.tools.reflection.withReflectionTools
51
+ import ai.ansight.tools.securestorage.withSecureStorageTools
52
+ import ai.ansight.tools.visualtree.withVisualTreeTools
53
+ import android.app.Activity
54
+ import android.app.Application
55
+ import android.util.Base64
56
+ import com.facebook.react.bridge.Arguments
57
+ import com.facebook.react.bridge.LifecycleEventListener
58
+ import com.facebook.react.bridge.Promise
59
+ import com.facebook.react.bridge.ReactApplicationContext
60
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
61
+ import com.facebook.react.bridge.ReactMethod
62
+ import com.facebook.react.bridge.ReadableArray
63
+ import com.facebook.react.bridge.ReadableMap
64
+ import com.facebook.react.bridge.ReadableType
65
+ import com.facebook.react.bridge.UiThreadUtil
66
+ import com.facebook.react.bridge.WritableArray
67
+ import com.facebook.react.bridge.WritableMap
68
+ import com.facebook.react.modules.core.DeviceEventManagerModule
69
+ import org.json.JSONArray
70
+ import org.json.JSONObject
71
+ import java.util.UUID
72
+ import java.util.concurrent.ConcurrentHashMap
73
+ import java.util.concurrent.CountDownLatch
74
+ import java.util.concurrent.Executors
75
+ import java.util.concurrent.TimeUnit
76
+ import java.util.concurrent.atomic.AtomicInteger
77
+ import java.util.Locale
78
+
79
+ class AnsightReactNativeModule(
80
+ private val reactContext: ReactApplicationContext,
81
+ ) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener {
82
+ private data class PendingToolCall(
83
+ val context: AndroidToolExecutionContext,
84
+ val latch: CountDownLatch = CountDownLatch(1),
85
+ @Volatile var result: AndroidToolResult? = null,
86
+ )
87
+
88
+ private data class CustomToolRegistration(
89
+ val definition: ToolDefinition,
90
+ val timeoutMilliseconds: Long,
91
+ )
92
+
93
+ private val backgroundExecutor = Executors.newCachedThreadPool()
94
+ private val pendingToolCalls = ConcurrentHashMap<String, PendingToolCall>()
95
+ private val customToolRegistrations = ConcurrentHashMap<String, CustomToolRegistration>()
96
+ private val activeCustomToolIds = ConcurrentHashMap.newKeySet<String>()
97
+ private val listenerCount = AtomicInteger(0)
98
+ private val reactNativeMemoryProfiler = ReactNativeMemoryProfiler(reactContext)
99
+ private var currentReactNativeMemoryOptions = ReactNativeMemoryProfilingOptions.Defaults
100
+ private val logCallback = AnsightLogCallback { level, message, throwable ->
101
+ emitLogEvent(level, message, throwable)
102
+ }
103
+
104
+ init {
105
+ reactContext.addLifecycleEventListener(this)
106
+ AnsightLogger.registerCallback(logCallback)
107
+ }
108
+
109
+ override fun getName(): String = "AnsightReactNative"
110
+
111
+ override fun invalidate() {
112
+ AnsightLogger.removeCallback(logCallback)
113
+ reactContext.removeLifecycleEventListener(this)
114
+ super.invalidate()
115
+ }
116
+
117
+ override fun onHostResume() {
118
+ bindCurrentActivity()
119
+ }
120
+
121
+ override fun onHostPause() = Unit
122
+
123
+ override fun onHostDestroy() = Unit
124
+
125
+ @ReactMethod
126
+ fun addListener(eventName: String) {
127
+ listenerCount.incrementAndGet()
128
+ }
129
+
130
+ @ReactMethod
131
+ fun removeListeners(count: Int) {
132
+ listenerCount.updateAndGet { current -> (current - count).coerceAtLeast(0) }
133
+ }
134
+
135
+ @ReactMethod
136
+ fun initialize(options: ReadableMap?, promise: Promise) {
137
+ runCatching {
138
+ val runtimeOptions = buildOptions(options)
139
+ Ansight.initialize(application(), runtimeOptions)
140
+ configureReactNativeMemoryProfiling(options)
141
+ installRegisteredCustomTools()
142
+ snapshotMap()
143
+ }.resolve(promise)
144
+ }
145
+
146
+ @ReactMethod
147
+ fun initializeAndActivate(options: ReadableMap?, promise: Promise) {
148
+ runCatching {
149
+ val runtimeOptions = buildOptions(options)
150
+ Ansight.initializeAndActivate(application(), runtimeOptions)
151
+ configureReactNativeMemoryProfiling(options)
152
+ bindCurrentActivity()
153
+ installRegisteredCustomTools()
154
+ snapshotMap()
155
+ }.resolve(promise)
156
+ }
157
+
158
+ @ReactMethod
159
+ fun activate(promise: Promise) {
160
+ runCatching {
161
+ AnsightRuntime.activate()
162
+ bindCurrentActivity()
163
+ snapshotMap()
164
+ }.resolve(promise)
165
+ }
166
+
167
+ @ReactMethod
168
+ fun deactivate(promise: Promise) {
169
+ runCatching {
170
+ AnsightRuntime.deactivate()
171
+ snapshotMap()
172
+ }.resolve(promise)
173
+ }
174
+
175
+ @ReactMethod
176
+ fun clear(promise: Promise) {
177
+ runCatching {
178
+ AnsightRuntime.clear()
179
+ snapshotMap()
180
+ }.resolve(promise)
181
+ }
182
+
183
+ @ReactMethod
184
+ fun registerMetricChannel(channel: ReadableMap, promise: Promise) {
185
+ runCatching {
186
+ AnsightRuntime.registerMetricChannel(
187
+ AnsightChannel(
188
+ id = channel.intValue("id", -1),
189
+ name = channel.stringValue("name") ?: "",
190
+ unit = channel.stringValue("unit"),
191
+ type = channel.stringValue("type") ?: "custom",
192
+ colorHex = channel.stringValue("colorHex"),
193
+ source = channel.stringValue("source"),
194
+ group = channel.stringValue("group"),
195
+ kind = channel.stringValue("kind"),
196
+ ),
197
+ )
198
+ snapshotMap()
199
+ }.resolve(promise)
200
+ }
201
+
202
+ @ReactMethod
203
+ fun recordMetric(value: Double, channel: Double, promise: Promise) {
204
+ runCatching {
205
+ val channelId = if (channel.isNaN()) AnsightChannels.Unspecified else channel.toInt()
206
+ AnsightRuntime.metric(value.toLong(), channelId)
207
+ snapshotMap()
208
+ }.resolve(promise)
209
+ }
210
+
211
+ @ReactMethod
212
+ fun recordEvent(input: ReadableMap, promise: Promise) {
213
+ runCatching {
214
+ val label = input.stringValue("label") ?: ""
215
+ AnsightRuntime.event(
216
+ label = label,
217
+ type = eventType(input.stringValue("type")),
218
+ details = input.stringValue("details"),
219
+ channel = input.intValue("channel", AnsightChannels.Unspecified),
220
+ )
221
+ snapshotMap()
222
+ }.resolve(promise)
223
+ }
224
+
225
+ @ReactMethod
226
+ fun screenViewed(name: String, details: ReadableMap?, promise: Promise) {
227
+ runCatching {
228
+ AnsightRuntime.screenViewed(name, details.toStringMap())
229
+ snapshotMap()
230
+ }.resolve(promise)
231
+ }
232
+
233
+ @ReactMethod
234
+ fun setAppLifecycleState(state: String, promise: Promise) {
235
+ runCatching {
236
+ AnsightRuntime.setAppLifecycleState(lifecycleState(state))
237
+ snapshotMap()
238
+ }.resolve(promise)
239
+ }
240
+
241
+ @ReactMethod
242
+ fun connect(pairingPayload: String?, options: ReadableMap?, promise: Promise) {
243
+ backgroundExecutor.execute {
244
+ runCatching {
245
+ val request = if (pairingPayload.isNullOrBlank()) {
246
+ HostConnectionRequest(
247
+ kind = HostConnectionRequestKind.Auto,
248
+ clientName = options.stringValue("clientName"),
249
+ expectedAppId = options.stringValue("expectedAppId"),
250
+ hostAddressOverride = options.stringValue("hostAddressOverride"),
251
+ )
252
+ } else {
253
+ HostConnectionRequest(
254
+ kind = HostConnectionRequestKind.Payload,
255
+ payload = pairingPayload,
256
+ clientName = options.stringValue("clientName"),
257
+ expectedAppId = options.stringValue("expectedAppId"),
258
+ hostAddressOverride = options.stringValue("hostAddressOverride"),
259
+ )
260
+ }
261
+ hostConnectionResultMap(AnsightRuntime.connect(request))
262
+ }.resolve(promise)
263
+ }
264
+ }
265
+
266
+ @ReactMethod
267
+ fun openSession(pairingPayload: String?, options: ReadableMap?, promise: Promise) {
268
+ backgroundExecutor.execute {
269
+ runCatching {
270
+ openSessionResultMap(
271
+ AnsightRuntime.openSession(
272
+ pairingPayload.orEmpty(),
273
+ pairingOpenOptions(options),
274
+ ),
275
+ )
276
+ }.resolve(promise)
277
+ }
278
+ }
279
+
280
+ @ReactMethod
281
+ fun disconnect(promise: Promise) {
282
+ backgroundExecutor.execute {
283
+ runCatching { hostConnectionResultMap(AnsightRuntime.disconnect()) }.resolve(promise)
284
+ }
285
+ }
286
+
287
+ @ReactMethod
288
+ fun completeSession(promise: Promise) {
289
+ backgroundExecutor.execute {
290
+ runCatching {
291
+ AnsightRuntime.completeSession()
292
+ operationResultMap(OperationResult.success("Session completed."))
293
+ }.resolve(promise)
294
+ }
295
+ }
296
+
297
+ @ReactMethod
298
+ fun closeSession(promise: Promise) {
299
+ backgroundExecutor.execute {
300
+ runCatching {
301
+ AnsightRuntime.closeSession()
302
+ operationResultMap(OperationResult.success("Session closed."))
303
+ }.resolve(promise)
304
+ }
305
+ }
306
+
307
+ @ReactMethod
308
+ fun savePairingConfig(pairingPayload: String?, options: ReadableMap?, promise: Promise) {
309
+ backgroundExecutor.execute {
310
+ runCatching {
311
+ hostConnectionResultMap(
312
+ AnsightRuntime.savePairingConfig(
313
+ pairingPayload.orEmpty(),
314
+ options.stringValue("expectedAppId"),
315
+ ),
316
+ )
317
+ }.resolve(promise)
318
+ }
319
+ }
320
+
321
+ @ReactMethod
322
+ fun clearSavedPairing(promise: Promise) {
323
+ runCatching { hostConnectionResultMap(AnsightRuntime.clearSavedPairingConfig()) }.resolve(promise)
324
+ }
325
+
326
+ @ReactMethod
327
+ fun clearCachedSession(promise: Promise) {
328
+ runCatching { operationResultMap(AnsightRuntime.clearCachedSession()) }.resolve(promise)
329
+ }
330
+
331
+ @ReactMethod
332
+ fun notifyHostConnectionConfigChanged(promise: Promise) {
333
+ runCatching { hostConnectionResultMap(AnsightRuntime.notifyHostConnectionConfigChanged()) }.resolve(promise)
334
+ }
335
+
336
+ @ReactMethod
337
+ fun status(promise: Promise) {
338
+ runCatching { snapshotMap() }.resolve(promise)
339
+ }
340
+
341
+ @ReactMethod
342
+ fun snapshot(promise: Promise) {
343
+ runCatching { snapshotMap() }.resolve(promise)
344
+ }
345
+
346
+ @ReactMethod
347
+ fun hostConnectionStatus(promise: Promise) {
348
+ runCatching { hostConnectionStatusMap(AnsightRuntime.hostConnectionStatus()) }.resolve(promise)
349
+ }
350
+
351
+ @ReactMethod
352
+ fun hostConnectionCapabilities(promise: Promise) {
353
+ runCatching { hostConnectionCapabilitiesMap(AnsightRuntime.hostConnectionCapabilities()) }.resolve(promise)
354
+ }
355
+
356
+ @ReactMethod
357
+ fun currentOptions(promise: Promise) {
358
+ runCatching { optionsMap(AnsightRuntime.options()) }.resolve(promise)
359
+ }
360
+
361
+ @ReactMethod
362
+ fun recordedMetrics(limit: Double, promise: Promise) {
363
+ runCatching {
364
+ val metrics = AnsightRuntime.recordedMetrics()
365
+ metricsArray(metrics.takeLast(limit.toInt().takeIf { it > 0 } ?: metrics.size))
366
+ }.resolve(promise)
367
+ }
368
+
369
+ @ReactMethod
370
+ fun recordedEvents(limit: Double, promise: Promise) {
371
+ runCatching {
372
+ val events = AnsightRuntime.recordedEvents()
373
+ eventsArray(events.takeLast(limit.toInt().takeIf { it > 0 } ?: events.size))
374
+ }.resolve(promise)
375
+ }
376
+
377
+ @ReactMethod
378
+ fun sendClientLog(line: String, promise: Promise) {
379
+ runCatching { operationResultMap(AnsightRuntime.sendClientLog(line)) }.resolve(promise)
380
+ }
381
+
382
+ @ReactMethod
383
+ fun captureBuiltInTelemetrySample(promise: Promise) {
384
+ runCatching {
385
+ AnsightRuntime.captureBuiltInTelemetrySample()
386
+ snapshotMap()
387
+ }.resolve(promise)
388
+ }
389
+
390
+ @ReactMethod
391
+ fun isFramesPerSecondEnabled(promise: Promise) {
392
+ runCatching { AnsightRuntime.isFramesPerSecondEnabled() }.resolve(promise)
393
+ }
394
+
395
+ @ReactMethod
396
+ fun enableFramesPerSecond(promise: Promise) {
397
+ runCatching {
398
+ AnsightRuntime.enableFramesPerSecond()
399
+ snapshotMap()
400
+ }.resolve(promise)
401
+ }
402
+
403
+ @ReactMethod
404
+ fun disableFramesPerSecond(promise: Promise) {
405
+ runCatching {
406
+ AnsightRuntime.disableFramesPerSecond()
407
+ snapshotMap()
408
+ }.resolve(promise)
409
+ }
410
+
411
+ @ReactMethod
412
+ fun captureScreenFrame(options: ReadableMap?, promise: Promise) {
413
+ backgroundExecutor.execute {
414
+ runCatching {
415
+ bindCurrentActivity()
416
+ operationResultMap(AnsightRuntime.captureScreenFrame(sessionJpegCaptureOptions(options)))
417
+ }.resolve(promise)
418
+ }
419
+ }
420
+
421
+ @ReactMethod
422
+ fun enableTouchCapture(promise: Promise) {
423
+ runCatching { operationResultMap(AnsightRuntime.enableTouchCapture()) }.resolve(promise)
424
+ }
425
+
426
+ @ReactMethod
427
+ fun disableTouchCapture(promise: Promise) {
428
+ runCatching { operationResultMap(AnsightRuntime.disableTouchCapture()) }.resolve(promise)
429
+ }
430
+
431
+ @ReactMethod
432
+ fun updateSessionProperties(properties: ReadableMap?, promise: Promise) {
433
+ runCatching {
434
+ operationResultMap(AnsightRuntime.updateCustomProperties(properties.toGroupedStringMap()))
435
+ }.resolve(promise)
436
+ }
437
+
438
+ @ReactMethod
439
+ fun clearSessionProperties(promise: Promise) {
440
+ runCatching { operationResultMap(AnsightRuntime.clearCustomProperties()) }.resolve(promise)
441
+ }
442
+
443
+ @ReactMethod
444
+ fun registerCustomProperty(group: String, key: String, value: String, promise: Promise) {
445
+ runCatching { operationResultMap(AnsightRuntime.registerCustomProperty(group, key, value)) }.resolve(promise)
446
+ }
447
+
448
+ @ReactMethod
449
+ fun removeCustomProperty(group: String, key: String, promise: Promise) {
450
+ runCatching { operationResultMap(AnsightRuntime.removeCustomProperty(group, key)) }.resolve(promise)
451
+ }
452
+
453
+ @ReactMethod
454
+ fun registerCustomTool(definitionMap: ReadableMap, promise: Promise) {
455
+ runCatching {
456
+ val definition = toolDefinition(definitionMap)
457
+ val timeoutMilliseconds = definitionMap.intValue("timeoutMilliseconds", 30_000).toLong().coerceAtLeast(250L)
458
+ val registration = CustomToolRegistration(definition, timeoutMilliseconds)
459
+ customToolRegistrations[definition.id] = registration
460
+ activeCustomToolIds.add(definition.id)
461
+ if (AnsightRuntime.snapshot().initialized) {
462
+ installCustomTool(registration)
463
+ }
464
+ mapOf("id" to definition.id, "registered" to true).toWritableMap()
465
+ }.resolve(promise)
466
+ }
467
+
468
+ @ReactMethod
469
+ fun unregisterCustomTool(id: String, promise: Promise) {
470
+ activeCustomToolIds.remove(id.trim())
471
+ customToolRegistrations.remove(id.trim())
472
+ promise.resolve(mapOf("id" to id.trim(), "registered" to false).toWritableMap())
473
+ }
474
+
475
+ @ReactMethod
476
+ fun clearRegisteredCustomTools(promise: Promise) {
477
+ activeCustomToolIds.clear()
478
+ customToolRegistrations.clear()
479
+ promise.resolve(mapOf("cleared" to true).toWritableMap())
480
+ }
481
+
482
+ @ReactMethod
483
+ fun resolveToolCall(requestId: String, resultMap: ReadableMap, promise: Promise) {
484
+ val pending = pendingToolCalls[requestId]
485
+ if (pending == null) {
486
+ promise.resolve(mapOf("requestId" to requestId, "accepted" to false).toWritableMap())
487
+ return
488
+ }
489
+
490
+ val success = resultMap.booleanValue("success", true)
491
+ val message = resultMap.stringValue("message")
492
+ val errorCode = resultMap.stringValue("errorCode")
493
+ val payload = resultPayload(resultMap)
494
+ pending.result = if (success) {
495
+ AndroidToolResult.success(payload, message)
496
+ } else {
497
+ AndroidToolResult.failure(message ?: "JavaScript tool failed.", errorCode, payload)
498
+ }
499
+ pending.latch.countDown()
500
+ promise.resolve(mapOf("requestId" to requestId, "accepted" to true).toWritableMap())
501
+ }
502
+
503
+ @ReactMethod
504
+ fun queueBinaryTransfer(requestId: String, base64Data: String, chunkBytes: Int, promise: Promise) {
505
+ runCatching {
506
+ val pending = pendingToolCalls[requestId.trim()]
507
+ ?: return@runCatching operationResultMap(OperationResult.failure("Binary transfer requires an active JavaScript tool request.")).apply {
508
+ putString("errorCode", "artifact_request_unavailable")
509
+ }
510
+ val transport = pending.context.transport
511
+ ?: return@runCatching operationResultMap(OperationResult.failure("Binary transfers require an active pairing session.")).apply {
512
+ putString("errorCode", "artifact_transfer_unavailable")
513
+ }
514
+ val bytes = Base64.decode(base64Data, Base64.DEFAULT)
515
+ val normalizedChunkBytes = chunkBytes.coerceIn(1024, 512 * 1024)
516
+ val transferId = PairingFileTransferWireProtocol.newTransferId()
517
+ backgroundExecutor.execute {
518
+ transport.sendBinaryTransfer(transferId, bytes, normalizedChunkBytes)
519
+ }
520
+
521
+ operationResultMap(OperationResult.success("Binary transfer queued.")).apply {
522
+ putString("transferId", transferId)
523
+ putString("deliveryMode", "websocket_binary")
524
+ putString("wireProtocol", PairingFileTransferWireProtocol.ProtocolName)
525
+ putString("status", "queued")
526
+ putInt("chunkBytes", normalizedChunkBytes)
527
+ putInt("sizeBytes", bytes.size)
528
+ }
529
+ }.resolve(promise)
530
+ }
531
+
532
+ private fun executeJavaScriptTool(
533
+ toolId: String,
534
+ arguments: Map<String, String>,
535
+ context: AndroidToolExecutionContext,
536
+ timeoutMilliseconds: Long,
537
+ ): AndroidToolResult {
538
+ if (toolId !in activeCustomToolIds) {
539
+ return AndroidToolResult.failure("Tool '$toolId' is no longer registered in JavaScript.", "javascript_tool_unregistered")
540
+ }
541
+ if (listenerCount.get() <= 0 || !reactContext.hasActiveCatalystInstance()) {
542
+ return AndroidToolResult.failure("React Native JavaScript bridge is not listening for Ansight tool calls.", "javascript_bridge_unavailable")
543
+ }
544
+
545
+ val requestId = "android.${UUID.randomUUID().toString().replace("-", "")}"
546
+ val pending = PendingToolCall(context = context)
547
+ pendingToolCalls[requestId] = pending
548
+ emitToolCall(requestId, toolId, arguments, context)
549
+
550
+ val completed = pending.latch.await(timeoutMilliseconds, TimeUnit.MILLISECONDS)
551
+ pendingToolCalls.remove(requestId)
552
+ if (!completed) {
553
+ return AndroidToolResult.failure("JavaScript handler for tool '$toolId' timed out.", "javascript_tool_timeout")
554
+ }
555
+ return pending.result ?: AndroidToolResult.failure("JavaScript handler for tool '$toolId' returned no result.", "javascript_tool_empty_result")
556
+ }
557
+
558
+ private fun installRegisteredCustomTools() {
559
+ customToolRegistrations.values.forEach { registration ->
560
+ installCustomTool(registration)
561
+ }
562
+ }
563
+
564
+ private fun installCustomTool(registration: CustomToolRegistration) {
565
+ val definition = registration.definition
566
+ activeCustomToolIds.add(definition.id)
567
+ AnsightRuntime.registerTool(
568
+ FunctionAndroidTool(definition) { arguments, context ->
569
+ executeJavaScriptTool(definition.id, arguments, context, registration.timeoutMilliseconds)
570
+ },
571
+ replaceExisting = true,
572
+ )
573
+ }
574
+
575
+ private fun emitToolCall(
576
+ requestId: String,
577
+ toolId: String,
578
+ arguments: Map<String, String>,
579
+ context: AndroidToolExecutionContext,
580
+ ) {
581
+ val event = Arguments.createMap()
582
+ event.putString("requestId", requestId)
583
+ event.putString("toolId", toolId)
584
+ event.putString("platform", "android")
585
+ event.putString("sessionId", context.sessionId)
586
+ event.putString("nativeRequestId", context.requestId)
587
+ event.putMap("arguments", arguments.toWritableMap())
588
+ UiThreadUtil.runOnUiThread {
589
+ reactContext
590
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
591
+ .emit("AnsightToolCall", event)
592
+ }
593
+ }
594
+
595
+ private fun emitLogEvent(level: AnsightLogLevel, message: String, throwable: Throwable?) {
596
+ if (listenerCount.get() <= 0) {
597
+ return
598
+ }
599
+ val event = Arguments.createMap()
600
+ event.putString("level", level.name.lowercase(Locale.US))
601
+ event.putString("message", message)
602
+ event.putString("platform", "android")
603
+ throwable?.message?.let { event.putString("error", it) }
604
+ UiThreadUtil.runOnUiThread {
605
+ reactContext
606
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
607
+ .emit("AnsightLog", event)
608
+ }
609
+ }
610
+
611
+ private fun configureReactNativeMemoryProfiling(map: ReadableMap?) {
612
+ val options = reactNativeMemoryProfilingOptions(map)
613
+ currentReactNativeMemoryOptions = options
614
+ reactNativeMemoryProfiler.register(options)
615
+ }
616
+
617
+ private fun buildOptions(map: ReadableMap?): AnsightOptions {
618
+ val useNativeAllInOneDefaults = map.booleanValue("useNativeAllInOneDefaults", false)
619
+ val pairingConfigJson = map.stringValue("pairingConfigJson")
620
+ val clientName = map.stringValue("clientName")
621
+ var options = if (useNativeAllInOneDefaults) {
622
+ AnsightDeveloperMode.options(
623
+ bundledDeveloperConfigJson = pairingConfigJson,
624
+ clientName = clientName,
625
+ )
626
+ } else {
627
+ AnsightOptions()
628
+ }
629
+ if (!useNativeAllInOneDefaults && !pairingConfigJson.isNullOrBlank()) {
630
+ options = options.copy(
631
+ hostConnection = options.hostConnection.copy(
632
+ bundledConfigJson = pairingConfigJson,
633
+ ),
634
+ )
635
+ }
636
+ if (useNativeAllInOneDefaults && !map.hasString("toolGuard")) {
637
+ options = options.copy(toolGuard = AnsightToolGuard.ReadOnly)
638
+ }
639
+
640
+ if (map.hasNumber("sampleFrequencyMilliseconds")) {
641
+ options = options.copy(sampleFrequencyMilliseconds = map.intValue("sampleFrequencyMilliseconds", options.sampleFrequencyMilliseconds))
642
+ }
643
+ if (map.hasNumber("retentionPeriodSeconds")) {
644
+ options = options.copy(retentionPeriodSeconds = map.intValue("retentionPeriodSeconds", options.retentionPeriodSeconds))
645
+ }
646
+ if (map.hasBoolean("enableFramesPerSecond")) {
647
+ options = options.copy(enableFramesPerSecond = map.booleanValue("enableFramesPerSecond", options.enableFramesPerSecond))
648
+ }
649
+ if (map.hasBoolean("enableBatteryLevel")) {
650
+ options = options.copy(enableBatteryLevel = map.booleanValue("enableBatteryLevel", options.enableBatteryLevel))
651
+ }
652
+ if (map.hasMap("defaultMemoryChannels")) {
653
+ val memory = map.getMapOrNull("defaultMemoryChannels")
654
+ val managedHeap = if (memory.hasBoolean("managedHeap")) {
655
+ memory.booleanValue("managedHeap", options.defaultMemoryChannels.javaHeap)
656
+ } else {
657
+ memory.booleanValue("javaHeap", options.defaultMemoryChannels.javaHeap)
658
+ }
659
+ val residentSetSize = if (memory.hasBoolean("residentSetSize")) {
660
+ memory.booleanValue("residentSetSize", options.defaultMemoryChannels.rss)
661
+ } else {
662
+ memory.booleanValue("rss", options.defaultMemoryChannels.rss)
663
+ }
664
+ options = options.copy(
665
+ defaultMemoryChannels = DefaultMemoryChannels(
666
+ javaHeap = managedHeap,
667
+ nativeHeap = memory.booleanValue("nativeHeap", options.defaultMemoryChannels.nativeHeap),
668
+ rss = residentSetSize,
669
+ ),
670
+ )
671
+ }
672
+ if (map.hasArray("additionalChannels")) {
673
+ val channels = mutableListOf<AnsightChannel>()
674
+ val array = map.getArrayOrNull("additionalChannels")
675
+ if (array != null) {
676
+ for (index in 0 until array.size()) {
677
+ val channel = array.getMap(index)
678
+ channels += AnsightChannel(
679
+ id = channel.intValue("id", -1),
680
+ name = channel.stringValue("name") ?: "",
681
+ unit = channel.stringValue("unit"),
682
+ type = channel.stringValue("type") ?: "custom",
683
+ colorHex = channel.stringValue("colorHex"),
684
+ source = channel.stringValue("source"),
685
+ group = channel.stringValue("group"),
686
+ kind = channel.stringValue("kind"),
687
+ )
688
+ }
689
+ }
690
+ options = options.copy(additionalChannels = channels)
691
+ }
692
+ if (map.hasKey("sessionJpegCapture")) {
693
+ options = options.copy(
694
+ sessionJpegCapture = if (map.isFalse("sessionJpegCapture")) {
695
+ null
696
+ } else {
697
+ val jpeg = map.getMapOrNull("sessionJpegCapture")
698
+ AnsightSessionJpegCaptureOptions(
699
+ intervalMilliseconds = jpeg.intValue(
700
+ "intervalMilliseconds",
701
+ AnsightSessionJpegCaptureOptions.DefaultIntervalMilliseconds,
702
+ ),
703
+ quality = jpeg.intValue("quality", AnsightSessionJpegCaptureOptions.DefaultQuality),
704
+ maxWidth = jpeg.optionalInt("maxWidth") ?: AnsightSessionJpegCaptureOptions.DefaultMaxWidth,
705
+ )
706
+ },
707
+ )
708
+ }
709
+ if (map.hasKey("touchCapture")) {
710
+ options = options.copy(
711
+ touchCapture = if (map.isFalse("touchCapture")) {
712
+ null
713
+ } else {
714
+ val touch = map.getMapOrNull("touchCapture")
715
+ AnsightTouchCaptureOptions(
716
+ moveCaptureDistanceThreshold = touch.doubleValue("moveCaptureDistanceThreshold", 8.0),
717
+ moveCaptureFramesPerSecond = touch.intValue("moveCaptureFramesPerSecond", 20),
718
+ )
719
+ },
720
+ )
721
+ }
722
+ if (map.hasString("toolGuard")) {
723
+ options = options.copy(toolGuard = toolGuard(map.stringValue("toolGuard")))
724
+ }
725
+ if (map.hasMap("customProperties")) {
726
+ options = options.copy(customProperties = map.getMapOrNull("customProperties").toGroupedStringMap())
727
+ }
728
+ if (map.hasMap("hostAutoProbe")) {
729
+ val autoProbe = map.getMapOrNull("hostAutoProbe")
730
+ options = options.copy(
731
+ hostAutoProbe = AnsightHostAutoProbeOptions(
732
+ enabled = autoProbe.booleanValue("enabled", options.hostAutoProbe.enabled),
733
+ initialDelayMilliseconds = autoProbe.longValue("initialDelayMilliseconds", options.hostAutoProbe.initialDelayMilliseconds),
734
+ probeIntervalMilliseconds = autoProbe.longValue("probeIntervalMilliseconds", options.hostAutoProbe.probeIntervalMilliseconds),
735
+ reconnectDelayMilliseconds = autoProbe.longValue("reconnectDelayMilliseconds", options.hostAutoProbe.reconnectDelayMilliseconds),
736
+ clientName = autoProbe.stringValue("clientName") ?: options.hostAutoProbe.clientName,
737
+ ),
738
+ )
739
+ }
740
+ if (map.hasMap("hostConnection")) {
741
+ val host = map.getMapOrNull("hostConnection")
742
+ options = options.copy(
743
+ hostConnection = AnsightHostConnectionOptions(
744
+ savedConfigKey = host.stringValue("savedConfigKey") ?: options.hostConnection.savedConfigKey,
745
+ bundledConfigJson = host.stringValue("bundledConfigJson") ?: options.hostConnection.bundledConfigJson,
746
+ bundledDeveloperConfigJson = host.stringValue("bundledDeveloperConfigJson") ?: options.hostConnection.bundledDeveloperConfigJson,
747
+ discoveryPort = host.optionalInt("discoveryPort") ?: options.hostConnection.discoveryPort,
748
+ connectionProfileRetentionSeconds = host.longValue(
749
+ "connectionProfileRetentionSeconds",
750
+ options.hostConnection.connectionProfileRetentionSeconds,
751
+ ),
752
+ ),
753
+ )
754
+ }
755
+ if (map.hasMap("secureStorage")) {
756
+ val secure = map.getMapOrNull("secureStorage")
757
+ options = options.copy(
758
+ secureStorage = AnsightSecureStorageOptions(
759
+ preferencesName = secure.stringValue("preferencesName") ?: options.secureStorage.preferencesName,
760
+ allowedKeys = secure.getStringSet("allowedKeys"),
761
+ allowedPrefixes = secure.getStringSet("allowedPrefixes"),
762
+ ),
763
+ )
764
+ }
765
+ return options.withNativeToolOptions(map)
766
+ }
767
+
768
+ private fun AnsightOptions.withNativeToolOptions(map: ReadableMap?): AnsightOptions {
769
+ val remoteTools = map.getMapOrNull("remoteTools")
770
+ val builder = AnsightOptions.createBuilder(this)
771
+ if (remoteTools.toolSuiteEnabled("visualTree")) {
772
+ builder.withVisualTreeTools()
773
+ }
774
+ builder.withDatabaseTools(databaseToolsOptions(remoteTools.getMapOrNull("database")))
775
+ builder.withFileSystemTools(fileSystemToolsOptions(remoteTools.getMapOrNull("fileSystem")))
776
+ builder.withPreferencesTools(preferencesToolsOptions(remoteTools.getMapOrNull("preferences")))
777
+ builder.withReflectionTools(reflectionToolsOptions(remoteTools.getMapOrNull("reflection")))
778
+ builder.withSecureStorageTools(
779
+ secureStorageToolsOptions(
780
+ remoteTools.getMapOrNull("secureStorage") ?: map.getMapOrNull("secureStorage"),
781
+ secureStorage,
782
+ ),
783
+ )
784
+ return builder.build()
785
+ }
786
+
787
+ private fun fileSystemToolsOptions(map: ReadableMap?): AndroidFileSystemToolsOptions {
788
+ return AndroidFileSystemToolsOptions(
789
+ additionalRoots = rootOptions(map.getArrayOrNull("additionalRoots")).map { root ->
790
+ AndroidFileSystemRoot(root.alias, root.path)
791
+ },
792
+ ).validated()
793
+ }
794
+
795
+ private fun databaseToolsOptions(map: ReadableMap?): AndroidDatabaseToolsOptions {
796
+ return AndroidDatabaseToolsOptions(
797
+ additionalRoots = rootOptions(map.getArrayOrNull("additionalRoots")).map { root ->
798
+ AndroidDatabaseRoot(root.alias, root.path)
799
+ },
800
+ includePlatformRoots = map.booleanValue("includePlatformRoots", true),
801
+ ).validated()
802
+ }
803
+
804
+ private fun preferencesToolsOptions(map: ReadableMap?): AndroidPreferencesToolsOptions {
805
+ return AndroidPreferencesToolsOptions(
806
+ defaultStore = map.stringValue("defaultStore"),
807
+ allowedStores = map.getStringSet("allowedStores"),
808
+ allowedKeys = map.getStringSet("allowedKeys"),
809
+ allowedKeyPrefixes = map.getStringSet("allowedKeyPrefixes"),
810
+ ).validated()
811
+ }
812
+
813
+ private fun reflectionToolsOptions(map: ReadableMap?): AndroidReflectionToolsOptions {
814
+ return AndroidReflectionToolsOptions(
815
+ includeBuiltInRoots = map.booleanValue("includeBuiltInRoots", true),
816
+ allowedRootIds = map.getStringSet("allowedRootIds"),
817
+ allowedTypePrefixes = map.getStringSet("allowedTypePrefixes"),
818
+ ).validated()
819
+ }
820
+
821
+ private fun secureStorageToolsOptions(
822
+ map: ReadableMap?,
823
+ fallback: AnsightSecureStorageOptions,
824
+ ): AnsightSecureStorageOptions {
825
+ val allowedPrefixes = map.getStringSet("allowedKeyPrefixes") + map.getStringSet("allowedPrefixes")
826
+ return AnsightSecureStorageOptions(
827
+ preferencesName = map.stringValue("preferencesName") ?: fallback.preferencesName,
828
+ allowedKeys = map.getStringSet("allowedKeys"),
829
+ allowedPrefixes = allowedPrefixes,
830
+ ).validated()
831
+ }
832
+
833
+ private data class NativeToolRoot(val alias: String, val path: String)
834
+
835
+ private fun rootOptions(array: ReadableArray?): List<NativeToolRoot> {
836
+ if (array == null) {
837
+ return emptyList()
838
+ }
839
+ val roots = mutableListOf<NativeToolRoot>()
840
+ for (index in 0 until array.size()) {
841
+ if (!array.isNull(index) && array.getType(index) == ReadableType.Map) {
842
+ val root = array.getMap(index)
843
+ val alias = root.stringValue("alias")
844
+ val path = root.stringValue("path")
845
+ if (alias != null && path != null) {
846
+ roots += NativeToolRoot(alias, path)
847
+ }
848
+ }
849
+ }
850
+ return roots
851
+ }
852
+
853
+ private fun pairingOpenOptions(map: ReadableMap?): PairingOpenOptions =
854
+ PairingOpenOptions(
855
+ clientName = map.stringValue("clientName") ?: "React Native",
856
+ expectedAppId = map.stringValue("expectedAppId"),
857
+ hostAddressOverride = map.stringValue("hostAddressOverride"),
858
+ )
859
+
860
+ private fun sessionJpegCaptureOptions(map: ReadableMap?): AnsightSessionJpegCaptureOptions? {
861
+ if (map == null) {
862
+ return null
863
+ }
864
+ return AnsightSessionJpegCaptureOptions(
865
+ intervalMilliseconds = map.intValue(
866
+ "intervalMilliseconds",
867
+ AnsightSessionJpegCaptureOptions.DefaultIntervalMilliseconds,
868
+ ),
869
+ quality = map.intValue("quality", AnsightSessionJpegCaptureOptions.DefaultQuality),
870
+ maxWidth = map.optionalInt("maxWidth") ?: AnsightSessionJpegCaptureOptions.DefaultMaxWidth,
871
+ )
872
+ }
873
+
874
+ private fun toolDefinition(map: ReadableMap): ToolDefinition =
875
+ ToolDefinition(
876
+ id = map.stringValue("id") ?: "",
877
+ name = map.stringValue("name") ?: map.stringValue("id") ?: "",
878
+ description = map.stringValue("description") ?: "",
879
+ category = map.stringValue("category") ?: "custom",
880
+ scope = toolScope(map.stringValue("scope")),
881
+ keywords = when {
882
+ map.hasArray("keywords") -> map.getArray("keywords").toStringList().joinToString(" ")
883
+ else -> map.stringValue("keywords") ?: "react native custom tool"
884
+ },
885
+ argumentsSchema = schemaFrom(map.getMapOrNull("argumentsSchema")),
886
+ resultSchema = schemaFrom(map.getMapOrNull("resultSchema")),
887
+ security = toolSecurity(map.getMapOrNull("security")),
888
+ ).validated()
889
+
890
+ private fun schemaFrom(map: ReadableMap?): ToolSchema {
891
+ if (map == null) {
892
+ return ToolSchema.obj(additionalProperties = true)
893
+ }
894
+ val type = when {
895
+ map.hasArray("type") -> map.getArray("type").toStringList().firstOrNull { it != "null" } ?: "object"
896
+ else -> map.stringValue("type") ?: "object"
897
+ }
898
+ val properties = mutableMapOf<String, ToolSchema>()
899
+ map.getMapOrNull("properties")?.let { props ->
900
+ val iterator = props.keySetIterator()
901
+ while (iterator.hasNextKey()) {
902
+ val key = iterator.nextKey()
903
+ props.getMapOrNull(key)?.let { properties[key] = schemaFrom(it) }
904
+ }
905
+ }
906
+ return ToolSchema(
907
+ type = type,
908
+ description = map.stringValue("description"),
909
+ properties = properties,
910
+ required = map.getArrayOrNull("required").toStringList(),
911
+ items = schemaFrom(map.getMapOrNull("items")).takeIf { map.hasMap("items") },
912
+ enumValues = map.getArrayOrNull("enum").toStringList(),
913
+ additionalProperties = map.booleanValue("additionalProperties", false),
914
+ nullable = map.hasArray("type") && map.getArray("type").toStringList().contains("null"),
915
+ format = map.stringValue("format"),
916
+ )
917
+ }
918
+
919
+ private fun toolSecurity(map: ReadableMap?): ToolSecurity {
920
+ if (map == null) {
921
+ return ToolSecurity.Unspecified
922
+ }
923
+ return ToolSecurity(
924
+ level = when (map.stringValue("level")?.trim()?.lowercase()) {
925
+ "medium", "moderate" -> ToolSecurityLevel.Medium
926
+ "high" -> ToolSecurityLevel.High
927
+ "critical" -> ToolSecurityLevel.Critical
928
+ else -> ToolSecurityLevel.Low
929
+ },
930
+ implications = map.getArrayOrNull("implications").toStringList(),
931
+ )
932
+ }
933
+
934
+ private fun resultPayload(map: ReadableMap): JSONObject? {
935
+ if (!map.hasKey("result") || map.isNull("result")) {
936
+ return null
937
+ }
938
+ return when (map.getType("result")) {
939
+ ReadableType.Map -> map.getMap("result")?.toJSONObject()
940
+ ReadableType.Array -> JSONObject().put("value", map.getArray("result")?.toJSONArray() ?: JSONArray())
941
+ ReadableType.String -> JSONObject().put("value", map.getString("result"))
942
+ ReadableType.Number -> JSONObject().put("value", map.getDouble("result"))
943
+ ReadableType.Boolean -> JSONObject().put("value", map.getBoolean("result"))
944
+ else -> null
945
+ }
946
+ }
947
+
948
+ private fun snapshotMap(): WritableMap {
949
+ val snapshot = AnsightRuntime.snapshot()
950
+ return Arguments.createMap().apply {
951
+ putBoolean("initialized", snapshot.initialized)
952
+ putBoolean("active", snapshot.active)
953
+ putBoolean("sessionOpen", snapshot.sessionOpen)
954
+ putString("lifecycleState", snapshot.lifecycleState.wireName)
955
+ putString("lifecycleChangedAtUtc", snapshot.lifecycleChangedAtUtc)
956
+ putInt("metricsRecorded", snapshot.metricsRecorded)
957
+ putInt("eventsRecorded", snapshot.eventsRecorded)
958
+ putInt("touchesRecorded", snapshot.touchesRecorded)
959
+ putInt("registeredTools", snapshot.registeredTools)
960
+ putString("sessionMessage", snapshot.sessionMessage)
961
+ putMap("connectionStatus", hostConnectionStatusMap(snapshot.connectionStatus))
962
+ putArray("channels", channelsArray(snapshot.channels))
963
+ snapshot.lastMetric?.let { putMap("lastMetric", metricMap(it)) }
964
+ snapshot.lastEvent?.let { putMap("lastEvent", eventMap(it)) }
965
+ snapshot.currentScreen?.let { screen ->
966
+ putMap("currentScreen", Arguments.createMap().apply {
967
+ putString("name", screen.name)
968
+ putString("capturedAtUtc", screen.capturedAtUtc)
969
+ putMap("details", screen.details.toWritableMap())
970
+ })
971
+ }
972
+ }
973
+ }
974
+
975
+ private fun hostConnectionStatusMap(status: HostConnectionStatus): WritableMap =
976
+ Arguments.createMap().apply {
977
+ putBoolean("isRuntimeActive", status.isRuntimeActive)
978
+ putBoolean("isConnected", status.isConnected)
979
+ putString("connectionState", status.connectionState.name)
980
+ putBoolean("hasCachedSession", status.hasCachedSession)
981
+ putBoolean("hasSavedConfig", status.hasSavedConfig)
982
+ putBoolean("hasBundledConfig", status.hasBundledConfig)
983
+ putString("summaryKind", status.summaryKind.name)
984
+ putString("summaryMessage", status.summaryMessage)
985
+ }
986
+
987
+ private fun hostConnectionCapabilitiesMap(capabilities: HostConnectionCapabilities): WritableMap =
988
+ Arguments.createMap().apply {
989
+ putBoolean("canConnectUsingSavedConfig", capabilities.canConnectUsingSavedConfig)
990
+ putBoolean("canConnectUsingBundledConfig", capabilities.canConnectUsingBundledConfig)
991
+ putBoolean("canChooseConfigFile", capabilities.canChooseConfigFile)
992
+ putBoolean("canScanConfigQrCode", capabilities.canScanConfigQrCode)
993
+ putBoolean("canClearSavedConfigs", capabilities.canClearSavedConfigs)
994
+ }
995
+
996
+ private fun openSessionResultMap(result: OpenSessionResult): WritableMap =
997
+ operationResultMap(OperationResult(result.success, result.message)).apply {
998
+ putBoolean("accepted", result.accepted)
999
+ putString("sessionId", result.sessionId)
1000
+ putString("configId", result.configId)
1001
+ putString("appId", result.appId)
1002
+ putString("resolvedHostAddress", result.resolvedHostAddress)
1003
+ putBoolean("usedEmbeddedDeveloperPairing", result.usedEmbeddedDeveloperPairing)
1004
+ putString("discoverySource", result.discoverySource)
1005
+ putString("reasonCode", result.reasonCode)
1006
+ putString("hostId", result.hostId)
1007
+ putString("hostName", result.hostName)
1008
+ }
1009
+
1010
+ private fun hostConnectionResultMap(result: HostConnectionResult): WritableMap =
1011
+ operationResultMap(result).apply {
1012
+ putString("kind", result.kind.name)
1013
+ putString("source", result.source.name)
1014
+ putString("reasonCode", result.reasonCode ?: result.openSession?.reasonCode)
1015
+ result.openSession?.let { session ->
1016
+ putString("sessionId", session.sessionId)
1017
+ putString("configId", session.configId)
1018
+ putString("appId", session.appId)
1019
+ putString("resolvedHostAddress", session.resolvedHostAddress)
1020
+ putString("hostId", session.hostId)
1021
+ putString("hostName", session.hostName)
1022
+ putBoolean("accepted", session.accepted)
1023
+ putBoolean("usedEmbeddedDeveloperPairing", session.usedEmbeddedDeveloperPairing)
1024
+ putString("discoverySource", session.discoverySource)
1025
+ }
1026
+ }
1027
+
1028
+ private fun operationResultMap(result: OperationResult): WritableMap =
1029
+ Arguments.createMap().apply {
1030
+ putBoolean("success", result.success)
1031
+ putString("message", result.message)
1032
+ }
1033
+
1034
+ private fun operationResultMap(result: HostConnectionResult): WritableMap =
1035
+ Arguments.createMap().apply {
1036
+ putBoolean("success", result.success)
1037
+ putString("message", result.message)
1038
+ }
1039
+
1040
+ private fun optionsMap(options: AnsightOptions): WritableMap =
1041
+ Arguments.createMap().apply {
1042
+ putInt("sampleFrequencyMilliseconds", options.sampleFrequencyMilliseconds)
1043
+ putInt("retentionPeriodSeconds", options.retentionPeriodSeconds)
1044
+ putBoolean("enableFramesPerSecond", options.enableFramesPerSecond)
1045
+ putBoolean("enableBatteryLevel", options.enableBatteryLevel)
1046
+ putMap("defaultMemoryChannels", mapOf(
1047
+ "managedHeap" to options.defaultMemoryChannels.javaHeap,
1048
+ "javaHeap" to options.defaultMemoryChannels.javaHeap,
1049
+ "nativeHeap" to options.defaultMemoryChannels.nativeHeap,
1050
+ "residentSetSize" to options.defaultMemoryChannels.rss,
1051
+ "rss" to options.defaultMemoryChannels.rss,
1052
+ "physicalFootprint" to false,
1053
+ ).toWritableMap())
1054
+ putMap("reactNativeMemory", currentReactNativeMemoryOptions.toMap().toWritableMap())
1055
+ putArray("additionalChannels", channelsArray(options.additionalChannels))
1056
+ options.sessionJpegCapture?.let { capture ->
1057
+ putMap("sessionJpegCapture", mapOf(
1058
+ "intervalMilliseconds" to capture.intervalMilliseconds,
1059
+ "quality" to capture.quality,
1060
+ "maxWidth" to capture.maxWidth,
1061
+ ).toWritableMap())
1062
+ } ?: putNull("sessionJpegCapture")
1063
+ options.touchCapture?.let { touch ->
1064
+ putMap("touchCapture", mapOf(
1065
+ "moveCaptureDistanceThreshold" to touch.moveCaptureDistanceThreshold,
1066
+ "moveCaptureFramesPerSecond" to touch.moveCaptureFramesPerSecond,
1067
+ ).toWritableMap())
1068
+ } ?: putNull("touchCapture")
1069
+ putString("toolGuard", toolGuardName(options.toolGuard))
1070
+ putMap("customProperties", options.customProperties.toGroupedWritableMap())
1071
+ putMap("hostAutoProbe", mapOf(
1072
+ "enabled" to options.hostAutoProbe.enabled,
1073
+ "initialDelayMilliseconds" to options.hostAutoProbe.initialDelayMilliseconds,
1074
+ "probeIntervalMilliseconds" to options.hostAutoProbe.probeIntervalMilliseconds,
1075
+ "reconnectDelayMilliseconds" to options.hostAutoProbe.reconnectDelayMilliseconds,
1076
+ "clientName" to options.hostAutoProbe.clientName,
1077
+ ).toWritableMap())
1078
+ putMap("hostConnection", mapOf(
1079
+ "savedConfigKey" to options.hostConnection.savedConfigKey,
1080
+ "hasBundledConfigJson" to (options.hostConnection.bundledConfigJson != null),
1081
+ "hasBundledDeveloperConfigJson" to (options.hostConnection.bundledDeveloperConfigJson != null),
1082
+ "discoveryPort" to options.hostConnection.discoveryPort,
1083
+ "connectionProfileRetentionSeconds" to options.hostConnection.connectionProfileRetentionSeconds,
1084
+ ).toWritableMap())
1085
+ putMap("secureStorage", mapOf(
1086
+ "preferencesName" to options.secureStorage.preferencesName,
1087
+ "allowedKeys" to options.secureStorage.allowedKeys.sorted().joinToString(","),
1088
+ "allowedPrefixes" to options.secureStorage.allowedPrefixes.sorted().joinToString(","),
1089
+ ).toWritableMap())
1090
+ }
1091
+
1092
+ private fun channelMap(channel: AnsightChannel): WritableMap =
1093
+ Arguments.createMap().apply {
1094
+ putInt("id", channel.id)
1095
+ putString("name", channel.name)
1096
+ putString("unit", channel.unit)
1097
+ putString("type", channel.type)
1098
+ putString("colorHex", channel.colorHex)
1099
+ putString("source", channel.source)
1100
+ putString("group", channel.group)
1101
+ putString("kind", channel.kind)
1102
+ }
1103
+
1104
+ private fun channelsArray(channels: List<AnsightChannel>): WritableArray =
1105
+ Arguments.createArray().apply {
1106
+ channels.forEach { pushMap(channelMap(it)) }
1107
+ }
1108
+
1109
+ private fun metricsArray(metrics: List<RecordedMetric>): WritableArray =
1110
+ Arguments.createArray().apply {
1111
+ metrics.forEach { pushMap(metricMap(it)) }
1112
+ }
1113
+
1114
+ private fun eventsArray(events: List<RecordedEvent>): WritableArray =
1115
+ Arguments.createArray().apply {
1116
+ events.forEach { pushMap(eventMap(it)) }
1117
+ }
1118
+
1119
+ private fun metricMap(metric: RecordedMetric): WritableMap =
1120
+ Arguments.createMap().apply {
1121
+ putDouble("value", metric.value.toDouble())
1122
+ putString("capturedAtUtc", metric.capturedAtUtc)
1123
+ putDouble("capturedAtEpochMs", metric.capturedAtEpochMs.toDouble())
1124
+ putInt("channel", metric.channel)
1125
+ putDouble("sequence", metric.sequence.toDouble())
1126
+ }
1127
+
1128
+ private fun eventMap(event: RecordedEvent): WritableMap =
1129
+ Arguments.createMap().apply {
1130
+ putString("id", event.id)
1131
+ putString("label", event.label)
1132
+ putString("type", event.type.wireName)
1133
+ putString("details", event.details)
1134
+ putString("capturedAtUtc", event.capturedAtUtc)
1135
+ putDouble("capturedAtEpochMs", event.capturedAtEpochMs.toDouble())
1136
+ putString("externalId", event.externalId)
1137
+ putInt("channel", event.channel)
1138
+ putDouble("sequence", event.sequence.toDouble())
1139
+ }
1140
+
1141
+ private fun application(): Application =
1142
+ reactContext.applicationContext as? Application
1143
+ ?: error("React Native application context is not an Android Application.")
1144
+
1145
+ private fun bindCurrentActivity() {
1146
+ val activity: Activity = currentActivity ?: return
1147
+ AnsightRuntime.bindActivity(activity)
1148
+ }
1149
+ }
1150
+
1151
+ private fun <T> Result<T>.resolve(promise: Promise) {
1152
+ fold(
1153
+ onSuccess = { promise.resolve(it) },
1154
+ onFailure = { promise.reject("ansight_error", it.message, it) },
1155
+ )
1156
+ }
1157
+
1158
+ private fun eventType(raw: String?): ai.ansight.runtime.AnsightEventType =
1159
+ when (raw?.trim()?.lowercase()) {
1160
+ "event" -> ai.ansight.runtime.AnsightEventType.Event
1161
+ "debug" -> ai.ansight.runtime.AnsightEventType.Debug
1162
+ "warning", "warn" -> ai.ansight.runtime.AnsightEventType.Warning
1163
+ "error" -> ai.ansight.runtime.AnsightEventType.Error
1164
+ "exception" -> ai.ansight.runtime.AnsightEventType.Exception
1165
+ "gc" -> ai.ansight.runtime.AnsightEventType.Gc
1166
+ "navigation" -> ai.ansight.runtime.AnsightEventType.Navigation
1167
+ "screenviewed", "screen_viewed" -> ai.ansight.runtime.AnsightEventType.ScreenViewed
1168
+ "lifecycle" -> ai.ansight.runtime.AnsightEventType.Lifecycle
1169
+ else -> ai.ansight.runtime.AnsightEventType.Info
1170
+ }
1171
+
1172
+ private fun lifecycleState(raw: String): AppLifecycleState =
1173
+ when (raw.trim().lowercase()) {
1174
+ "foreground", "active" -> AppLifecycleState.Foreground
1175
+ "background", "inactive" -> AppLifecycleState.Background
1176
+ else -> AppLifecycleState.Unknown
1177
+ }
1178
+
1179
+ private fun toolGuard(raw: String?): AnsightToolGuard =
1180
+ when (raw?.trim()?.lowercase()) {
1181
+ "readonly", "read_only", "read" -> AnsightToolGuard.ReadOnly
1182
+ "readwrite", "read_write", "write" -> AnsightToolGuard.ReadWrite
1183
+ "full", "fullaccess", "full_access" -> AnsightToolGuard.FullAccess
1184
+ else -> AnsightToolGuard.Disabled
1185
+ }
1186
+
1187
+ private fun toolGuardName(guard: AnsightToolGuard): String =
1188
+ when (guard) {
1189
+ AnsightToolGuard.Disabled -> "disabled"
1190
+ AnsightToolGuard.ReadOnly -> "readOnly"
1191
+ AnsightToolGuard.ReadWrite -> "readWrite"
1192
+ AnsightToolGuard.FullAccess -> "fullAccess"
1193
+ }
1194
+
1195
+ private fun toolScope(raw: String?): ToolScope =
1196
+ when (raw?.trim()?.lowercase()) {
1197
+ "write" -> ToolScope.Write
1198
+ "delete" -> ToolScope.Delete
1199
+ else -> ToolScope.Read
1200
+ }
1201
+
1202
+ private fun ReadableMap?.hasKey(name: String): Boolean = this?.hasKey(name) == true
1203
+ private fun ReadableMap?.hasString(name: String): Boolean = this?.hasKey(name) == true && this.getType(name) == ReadableType.String
1204
+ private fun ReadableMap?.hasNumber(name: String): Boolean = this?.hasKey(name) == true && this.getType(name) == ReadableType.Number
1205
+ private fun ReadableMap?.hasBoolean(name: String): Boolean = this?.hasKey(name) == true && this.getType(name) == ReadableType.Boolean
1206
+ private fun ReadableMap?.hasMap(name: String): Boolean = this?.hasKey(name) == true && this.getType(name) == ReadableType.Map
1207
+ private fun ReadableMap?.hasArray(name: String): Boolean = this?.hasKey(name) == true && this.getType(name) == ReadableType.Array
1208
+ private fun ReadableMap?.isFalse(name: String): Boolean = this?.hasBoolean(name) == true && !this.getBoolean(name)
1209
+ private fun ReadableMap?.toolSuiteEnabled(name: String): Boolean {
1210
+ val map = this ?: return false
1211
+ if (!map.hasKey(name) || map.isNull(name)) {
1212
+ return false
1213
+ }
1214
+ return when (map.getType(name)) {
1215
+ ReadableType.Boolean -> map.getBoolean(name)
1216
+ ReadableType.Map -> map.getMap(name).booleanValue("enabled", true)
1217
+ else -> false
1218
+ }
1219
+ }
1220
+ private fun ReadableMap?.stringValue(name: String): String? =
1221
+ if (this?.hasKey(name) == true && !isNull(name) && getType(name) == ReadableType.String) getString(name)?.trim()?.ifBlank { null } else null
1222
+ private fun ReadableMap?.booleanValue(name: String, default: Boolean): Boolean =
1223
+ if (this?.hasKey(name) == true && !isNull(name) && getType(name) == ReadableType.Boolean) getBoolean(name) else default
1224
+ private fun ReadableMap?.doubleValue(name: String, default: Double): Double =
1225
+ if (this?.hasKey(name) == true && !isNull(name) && getType(name) == ReadableType.Number) getDouble(name) else default
1226
+ private fun ReadableMap?.intValue(name: String, default: Int): Int =
1227
+ if (this?.hasKey(name) == true && !isNull(name) && getType(name) == ReadableType.Number) getDouble(name).toInt() else default
1228
+ private fun ReadableMap?.longValue(name: String, default: Long): Long =
1229
+ if (this?.hasKey(name) == true && !isNull(name) && getType(name) == ReadableType.Number) getDouble(name).toLong() else default
1230
+ private fun ReadableMap?.optionalInt(name: String): Int? =
1231
+ if (this?.hasKey(name) == true && !isNull(name) && getType(name) == ReadableType.Number) getDouble(name).toInt() else null
1232
+ private fun ReadableMap?.getMapOrNull(name: String): ReadableMap? =
1233
+ if (this?.hasKey(name) == true && !isNull(name) && getType(name) == ReadableType.Map) getMap(name) else null
1234
+ private fun ReadableMap?.getArrayOrNull(name: String): ReadableArray? =
1235
+ if (this?.hasKey(name) == true && !isNull(name) && getType(name) == ReadableType.Array) getArray(name) else null
1236
+
1237
+ private fun ReadableMap?.toStringMap(): Map<String, String> {
1238
+ if (this == null) {
1239
+ return emptyMap()
1240
+ }
1241
+ val result = linkedMapOf<String, String>()
1242
+ val iterator = keySetIterator()
1243
+ while (iterator.hasNextKey()) {
1244
+ val key = iterator.nextKey()
1245
+ if (!isNull(key)) {
1246
+ result[key] = when (getType(key)) {
1247
+ ReadableType.String -> getString(key).orEmpty()
1248
+ ReadableType.Number -> getDouble(key).toString()
1249
+ ReadableType.Boolean -> getBoolean(key).toString()
1250
+ else -> ""
1251
+ }
1252
+ }
1253
+ }
1254
+ return result
1255
+ }
1256
+
1257
+ private fun ReadableMap?.toGroupedStringMap(): Map<String, Map<String, String>> {
1258
+ if (this == null) {
1259
+ return emptyMap()
1260
+ }
1261
+ val result = linkedMapOf<String, Map<String, String>>()
1262
+ val iterator = keySetIterator()
1263
+ while (iterator.hasNextKey()) {
1264
+ val key = iterator.nextKey()
1265
+ if (!isNull(key) && getType(key) == ReadableType.Map) {
1266
+ result[key] = getMap(key).toStringMap()
1267
+ }
1268
+ }
1269
+ return result
1270
+ }
1271
+
1272
+ private fun ReadableMap?.getStringSet(name: String): Set<String> =
1273
+ getArrayOrNull(name).toStringList().toSet()
1274
+
1275
+ private fun ReadableArray?.toStringList(): List<String> {
1276
+ if (this == null) {
1277
+ return emptyList()
1278
+ }
1279
+ val result = mutableListOf<String>()
1280
+ for (index in 0 until size()) {
1281
+ if (!isNull(index)) {
1282
+ result += when (getType(index)) {
1283
+ ReadableType.String -> getString(index).orEmpty()
1284
+ ReadableType.Number -> getDouble(index).toString()
1285
+ ReadableType.Boolean -> getBoolean(index).toString()
1286
+ else -> ""
1287
+ }
1288
+ }
1289
+ }
1290
+ return result.filter { it.isNotBlank() }
1291
+ }
1292
+
1293
+ private fun ReadableMap.toJSONObject(): JSONObject {
1294
+ val result = JSONObject()
1295
+ val iterator = keySetIterator()
1296
+ while (iterator.hasNextKey()) {
1297
+ val key = iterator.nextKey()
1298
+ if (isNull(key)) {
1299
+ result.put(key, JSONObject.NULL)
1300
+ } else {
1301
+ when (getType(key)) {
1302
+ ReadableType.Map -> result.put(key, getMap(key)?.toJSONObject() ?: JSONObject.NULL)
1303
+ ReadableType.Array -> result.put(key, getArray(key)?.toJSONArray() ?: JSONObject.NULL)
1304
+ ReadableType.String -> result.put(key, getString(key))
1305
+ ReadableType.Number -> result.put(key, getDouble(key))
1306
+ ReadableType.Boolean -> result.put(key, getBoolean(key))
1307
+ else -> result.put(key, JSONObject.NULL)
1308
+ }
1309
+ }
1310
+ }
1311
+ return result
1312
+ }
1313
+
1314
+ private fun ReadableArray.toJSONArray(): JSONArray {
1315
+ val result = JSONArray()
1316
+ for (index in 0 until size()) {
1317
+ if (isNull(index)) {
1318
+ result.put(JSONObject.NULL)
1319
+ } else {
1320
+ when (getType(index)) {
1321
+ ReadableType.Map -> result.put(getMap(index)?.toJSONObject() ?: JSONObject.NULL)
1322
+ ReadableType.Array -> result.put(getArray(index)?.toJSONArray() ?: JSONObject.NULL)
1323
+ ReadableType.String -> result.put(getString(index))
1324
+ ReadableType.Number -> result.put(getDouble(index))
1325
+ ReadableType.Boolean -> result.put(getBoolean(index))
1326
+ else -> result.put(JSONObject.NULL)
1327
+ }
1328
+ }
1329
+ }
1330
+ return result
1331
+ }
1332
+
1333
+ private fun Map<String, *>.toWritableMap(): WritableMap =
1334
+ Arguments.createMap().also { map ->
1335
+ entries.sortedBy { it.key }.forEach { (key, value) ->
1336
+ when (value) {
1337
+ is Boolean -> map.putBoolean(key, value)
1338
+ is Int -> map.putInt(key, value)
1339
+ is Double -> map.putDouble(key, value)
1340
+ is Number -> map.putDouble(key, value.toDouble())
1341
+ is String -> map.putString(key, value)
1342
+ null -> map.putNull(key)
1343
+ else -> map.putString(key, value.toString())
1344
+ }
1345
+ }
1346
+ }
1347
+
1348
+ private fun Map<String, Map<String, String>>.toGroupedWritableMap(): WritableMap =
1349
+ Arguments.createMap().also { map ->
1350
+ entries.sortedBy { it.key }.forEach { (key, value) ->
1351
+ map.putMap(key, value.toWritableMap())
1352
+ }
1353
+ }