@rejourneyco/react-native 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +29 -0
  2. package/android/build.gradle.kts +135 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/proguard-rules.pro +1 -0
  5. package/android/src/main/AndroidManifest.xml +15 -0
  6. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
  7. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
  8. package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
  9. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
  10. package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
  11. package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
  12. package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
  13. package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
  14. package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
  15. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
  16. package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
  17. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
  18. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
  19. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
  20. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
  21. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
  22. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
  23. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
  24. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
  25. package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
  26. package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
  27. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
  28. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  29. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
  30. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  31. package/ios/Engine/DeviceRegistrar.swift +288 -0
  32. package/ios/Engine/DiagnosticLog.swift +387 -0
  33. package/ios/Engine/RejourneyImpl.swift +719 -0
  34. package/ios/Recording/AnrSentinel.swift +142 -0
  35. package/ios/Recording/EventBuffer.swift +326 -0
  36. package/ios/Recording/InteractionRecorder.swift +428 -0
  37. package/ios/Recording/ReplayOrchestrator.swift +624 -0
  38. package/ios/Recording/SegmentDispatcher.swift +492 -0
  39. package/ios/Recording/StabilityMonitor.swift +223 -0
  40. package/ios/Recording/TelemetryPipeline.swift +547 -0
  41. package/ios/Recording/ViewHierarchyScanner.swift +156 -0
  42. package/ios/Recording/VisualCapture.swift +675 -0
  43. package/ios/Rejourney.h +38 -0
  44. package/ios/Rejourney.mm +375 -0
  45. package/ios/Utility/DataCompression.swift +55 -0
  46. package/ios/Utility/ImageBlur.swift +89 -0
  47. package/ios/Utility/RuntimeMethodSwap.swift +41 -0
  48. package/ios/Utility/ViewIdentifier.swift +37 -0
  49. package/lib/commonjs/NativeRejourney.js +40 -0
  50. package/lib/commonjs/components/Mask.js +88 -0
  51. package/lib/commonjs/index.js +1443 -0
  52. package/lib/commonjs/sdk/autoTracking.js +1087 -0
  53. package/lib/commonjs/sdk/constants.js +166 -0
  54. package/lib/commonjs/sdk/errorTracking.js +187 -0
  55. package/lib/commonjs/sdk/index.js +50 -0
  56. package/lib/commonjs/sdk/metricsTracking.js +205 -0
  57. package/lib/commonjs/sdk/navigation.js +128 -0
  58. package/lib/commonjs/sdk/networkInterceptor.js +375 -0
  59. package/lib/commonjs/sdk/utils.js +433 -0
  60. package/lib/commonjs/sdk/version.js +13 -0
  61. package/lib/commonjs/types/expo-router.d.js +2 -0
  62. package/lib/commonjs/types/index.js +2 -0
  63. package/lib/module/NativeRejourney.js +38 -0
  64. package/lib/module/components/Mask.js +83 -0
  65. package/lib/module/index.js +1341 -0
  66. package/lib/module/sdk/autoTracking.js +1059 -0
  67. package/lib/module/sdk/constants.js +154 -0
  68. package/lib/module/sdk/errorTracking.js +177 -0
  69. package/lib/module/sdk/index.js +26 -0
  70. package/lib/module/sdk/metricsTracking.js +187 -0
  71. package/lib/module/sdk/navigation.js +120 -0
  72. package/lib/module/sdk/networkInterceptor.js +364 -0
  73. package/lib/module/sdk/utils.js +412 -0
  74. package/lib/module/sdk/version.js +7 -0
  75. package/lib/module/types/expo-router.d.js +2 -0
  76. package/lib/module/types/index.js +2 -0
  77. package/lib/typescript/NativeRejourney.d.ts +160 -0
  78. package/lib/typescript/components/Mask.d.ts +54 -0
  79. package/lib/typescript/index.d.ts +117 -0
  80. package/lib/typescript/sdk/autoTracking.d.ts +226 -0
  81. package/lib/typescript/sdk/constants.d.ts +138 -0
  82. package/lib/typescript/sdk/errorTracking.d.ts +47 -0
  83. package/lib/typescript/sdk/index.d.ts +24 -0
  84. package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
  85. package/lib/typescript/sdk/navigation.d.ts +48 -0
  86. package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
  87. package/lib/typescript/sdk/utils.d.ts +193 -0
  88. package/lib/typescript/sdk/version.d.ts +6 -0
  89. package/lib/typescript/types/index.d.ts +618 -0
  90. package/package.json +122 -0
  91. package/rejourney.podspec +23 -0
  92. package/src/NativeRejourney.ts +185 -0
  93. package/src/components/Mask.tsx +93 -0
  94. package/src/index.ts +1555 -0
  95. package/src/sdk/autoTracking.ts +1245 -0
  96. package/src/sdk/constants.ts +155 -0
  97. package/src/sdk/errorTracking.ts +231 -0
  98. package/src/sdk/index.ts +25 -0
  99. package/src/sdk/metricsTracking.ts +227 -0
  100. package/src/sdk/navigation.ts +152 -0
  101. package/src/sdk/networkInterceptor.ts +423 -0
  102. package/src/sdk/utils.ts +442 -0
  103. package/src/sdk/version.ts +6 -0
  104. package/src/types/expo-router.d.ts +7 -0
  105. package/src/types/index.ts +709 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Copyright 2026 Rejourney
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ package com.rejourney.utility
18
+
19
+ import java.io.ByteArrayOutputStream
20
+ import java.util.zip.GZIPOutputStream
21
+
22
+ /**
23
+ * Data compression utilities
24
+ * Android implementation aligned with iOS DataCompression.swift
25
+ */
26
+ object DataCompression {
27
+
28
+ /**
29
+ * Compress data using gzip
30
+ */
31
+ fun gzipCompress(data: ByteArray): ByteArray? {
32
+ return try {
33
+ val bos = ByteArrayOutputStream()
34
+ GZIPOutputStream(bos).use { gzip ->
35
+ gzip.write(data)
36
+ }
37
+ bos.toByteArray()
38
+ } catch (e: Exception) {
39
+ null
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Compress string using gzip
45
+ */
46
+ fun gzipCompress(text: String): ByteArray? {
47
+ return gzipCompress(text.toByteArray(Charsets.UTF_8))
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Extension function for ByteArray gzip compression
53
+ */
54
+ fun ByteArray.gzipCompress(): ByteArray? {
55
+ return DataCompression.gzipCompress(this)
56
+ }
57
+
58
+ /**
59
+ * Extension function for String gzip compression
60
+ */
61
+ fun String.gzipCompress(): ByteArray? {
62
+ return DataCompression.gzipCompress(this)
63
+ }
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Copyright 2026 Rejourney
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ package com.rejourney.utility
18
+
19
+ import android.graphics.Bitmap
20
+ import android.graphics.Canvas
21
+ import android.graphics.ColorMatrix
22
+ import android.graphics.ColorMatrixColorFilter
23
+ import android.graphics.Paint
24
+ import android.renderscript.Allocation
25
+ import android.renderscript.Element
26
+ import android.renderscript.RenderScript
27
+ import android.renderscript.ScriptIntrinsicBlur
28
+ import android.content.Context
29
+ import kotlin.math.min
30
+ import kotlin.math.sqrt
31
+
32
+ /**
33
+ * Image blur utilities for privacy masking
34
+ * Android implementation aligned with iOS ImageBlur.swift
35
+ */
36
+ object ImageBlur {
37
+
38
+ private const val MAX_BLUR_RADIUS = 25f // RenderScript limit
39
+ private const val DEFAULT_BLUR_RADIUS = 15f
40
+
41
+ /**
42
+ * Apply Gaussian blur using RenderScript (fast, GPU-accelerated)
43
+ * Falls back to box blur if RenderScript unavailable
44
+ */
45
+ @Suppress("DEPRECATION")
46
+ fun applyGaussianBlur(
47
+ context: Context,
48
+ bitmap: Bitmap,
49
+ radius: Float = DEFAULT_BLUR_RADIUS
50
+ ): Bitmap {
51
+ val safeRadius = min(radius, MAX_BLUR_RADIUS).coerceAtLeast(1f)
52
+
53
+ return try {
54
+ applyRenderScriptBlur(context, bitmap, safeRadius)
55
+ } catch (e: Exception) {
56
+ // Fallback to stack blur
57
+ applyStackBlur(bitmap, safeRadius.toInt())
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Apply RenderScript Gaussian blur (deprecated but still works)
63
+ */
64
+ @Suppress("DEPRECATION")
65
+ private fun applyRenderScriptBlur(
66
+ context: Context,
67
+ bitmap: Bitmap,
68
+ radius: Float
69
+ ): Bitmap {
70
+ val outputBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
71
+
72
+ val rs = RenderScript.create(context)
73
+ val input = Allocation.createFromBitmap(rs, bitmap)
74
+ val output = Allocation.createFromBitmap(rs, outputBitmap)
75
+
76
+ val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
77
+ script.setRadius(radius)
78
+ script.setInput(input)
79
+ script.forEach(output)
80
+
81
+ output.copyTo(outputBitmap)
82
+
83
+ input.destroy()
84
+ output.destroy()
85
+ script.destroy()
86
+ rs.destroy()
87
+
88
+ return outputBitmap
89
+ }
90
+
91
+ /**
92
+ * Stack blur algorithm (fallback for non-RenderScript devices)
93
+ * Based on Mario Klingemann's algorithm
94
+ */
95
+ private fun applyStackBlur(bitmap: Bitmap, radius: Int): Bitmap {
96
+ val w = bitmap.width
97
+ val h = bitmap.height
98
+
99
+ val pixels = IntArray(w * h)
100
+ bitmap.getPixels(pixels, 0, w, 0, 0, w, h)
101
+
102
+ val wm = w - 1
103
+ val hm = h - 1
104
+ val wh = w * h
105
+ val div = radius + radius + 1
106
+
107
+ val r = IntArray(wh)
108
+ val g = IntArray(wh)
109
+ val b = IntArray(wh)
110
+
111
+ var rsum: Int
112
+ var gsum: Int
113
+ var bsum: Int
114
+ var x: Int
115
+ var y: Int
116
+ var i: Int
117
+ var p: Int
118
+ var yp: Int
119
+ var yi: Int
120
+ var yw: Int
121
+
122
+ val vmin = IntArray(maxOf(w, h))
123
+
124
+ var divsum = (div + 1) shr 1
125
+ divsum *= divsum
126
+ val dv = IntArray(256 * divsum)
127
+ for (i in 0 until 256 * divsum) {
128
+ dv[i] = i / divsum
129
+ }
130
+
131
+ yw = 0
132
+ yi = 0
133
+
134
+ val stack = Array(div) { IntArray(3) }
135
+ var stackpointer: Int
136
+ var stackstart: Int
137
+ var sir: IntArray
138
+ var rbs: Int
139
+ val r1 = radius + 1
140
+ var routsum: Int
141
+ var goutsum: Int
142
+ var boutsum: Int
143
+ var rinsum: Int
144
+ var ginsum: Int
145
+ var binsum: Int
146
+
147
+ y = 0
148
+ while (y < h) {
149
+ bsum = 0
150
+ gsum = 0
151
+ rsum = 0
152
+ boutsum = 0
153
+ goutsum = 0
154
+ routsum = 0
155
+ binsum = 0
156
+ ginsum = 0
157
+ rinsum = 0
158
+
159
+ i = -radius
160
+ while (i <= radius) {
161
+ p = pixels[yi + minOf(wm, maxOf(i, 0))]
162
+ sir = stack[i + radius]
163
+ sir[0] = (p and 0xff0000) shr 16
164
+ sir[1] = (p and 0x00ff00) shr 8
165
+ sir[2] = p and 0x0000ff
166
+ rbs = r1 - kotlin.math.abs(i)
167
+ rsum += sir[0] * rbs
168
+ gsum += sir[1] * rbs
169
+ bsum += sir[2] * rbs
170
+ if (i > 0) {
171
+ rinsum += sir[0]
172
+ ginsum += sir[1]
173
+ binsum += sir[2]
174
+ } else {
175
+ routsum += sir[0]
176
+ goutsum += sir[1]
177
+ boutsum += sir[2]
178
+ }
179
+ i++
180
+ }
181
+ stackpointer = radius
182
+
183
+ x = 0
184
+ while (x < w) {
185
+ r[yi] = dv[rsum]
186
+ g[yi] = dv[gsum]
187
+ b[yi] = dv[bsum]
188
+
189
+ rsum -= routsum
190
+ gsum -= goutsum
191
+ bsum -= boutsum
192
+
193
+ stackstart = stackpointer - radius + div
194
+ sir = stack[stackstart % div]
195
+
196
+ routsum -= sir[0]
197
+ goutsum -= sir[1]
198
+ boutsum -= sir[2]
199
+
200
+ if (y == 0) {
201
+ vmin[x] = minOf(x + radius + 1, wm)
202
+ }
203
+ p = pixels[yw + vmin[x]]
204
+
205
+ sir[0] = (p and 0xff0000) shr 16
206
+ sir[1] = (p and 0x00ff00) shr 8
207
+ sir[2] = p and 0x0000ff
208
+
209
+ rinsum += sir[0]
210
+ ginsum += sir[1]
211
+ binsum += sir[2]
212
+
213
+ rsum += rinsum
214
+ gsum += ginsum
215
+ bsum += binsum
216
+
217
+ stackpointer = (stackpointer + 1) % div
218
+ sir = stack[stackpointer % div]
219
+
220
+ routsum += sir[0]
221
+ goutsum += sir[1]
222
+ boutsum += sir[2]
223
+
224
+ rinsum -= sir[0]
225
+ ginsum -= sir[1]
226
+ binsum -= sir[2]
227
+
228
+ yi++
229
+ x++
230
+ }
231
+ yw += w
232
+ y++
233
+ }
234
+
235
+ x = 0
236
+ while (x < w) {
237
+ bsum = 0
238
+ gsum = 0
239
+ rsum = 0
240
+ boutsum = 0
241
+ goutsum = 0
242
+ routsum = 0
243
+ binsum = 0
244
+ ginsum = 0
245
+ rinsum = 0
246
+
247
+ yp = -radius * w
248
+
249
+ i = -radius
250
+ while (i <= radius) {
251
+ yi = maxOf(0, yp) + x
252
+
253
+ sir = stack[i + radius]
254
+
255
+ sir[0] = r[yi]
256
+ sir[1] = g[yi]
257
+ sir[2] = b[yi]
258
+
259
+ rbs = r1 - kotlin.math.abs(i)
260
+
261
+ rsum += r[yi] * rbs
262
+ gsum += g[yi] * rbs
263
+ bsum += b[yi] * rbs
264
+
265
+ if (i > 0) {
266
+ rinsum += sir[0]
267
+ ginsum += sir[1]
268
+ binsum += sir[2]
269
+ } else {
270
+ routsum += sir[0]
271
+ goutsum += sir[1]
272
+ boutsum += sir[2]
273
+ }
274
+
275
+ if (i < hm) {
276
+ yp += w
277
+ }
278
+ i++
279
+ }
280
+
281
+ yi = x
282
+ stackpointer = radius
283
+
284
+ y = 0
285
+ while (y < h) {
286
+ pixels[yi] = (0xff000000.toInt() and pixels[yi]) or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum]
287
+
288
+ rsum -= routsum
289
+ gsum -= goutsum
290
+ bsum -= boutsum
291
+
292
+ stackstart = stackpointer - radius + div
293
+ sir = stack[stackstart % div]
294
+
295
+ routsum -= sir[0]
296
+ goutsum -= sir[1]
297
+ boutsum -= sir[2]
298
+
299
+ if (x == 0) {
300
+ vmin[y] = minOf(y + r1, hm) * w
301
+ }
302
+ p = x + vmin[y]
303
+
304
+ sir[0] = r[p]
305
+ sir[1] = g[p]
306
+ sir[2] = b[p]
307
+
308
+ rinsum += sir[0]
309
+ ginsum += sir[1]
310
+ binsum += sir[2]
311
+
312
+ rsum += rinsum
313
+ gsum += ginsum
314
+ bsum += binsum
315
+
316
+ stackpointer = (stackpointer + 1) % div
317
+ sir = stack[stackpointer]
318
+
319
+ routsum += sir[0]
320
+ goutsum += sir[1]
321
+ boutsum += sir[2]
322
+
323
+ rinsum -= sir[0]
324
+ ginsum -= sir[1]
325
+ binsum -= sir[2]
326
+
327
+ yi += w
328
+ y++
329
+ }
330
+ x++
331
+ }
332
+
333
+ val result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
334
+ result.setPixels(pixels, 0, w, 0, 0, w, h)
335
+ return result
336
+ }
337
+
338
+ /**
339
+ * Apply pixelation blur (faster, for heavy privacy masking)
340
+ */
341
+ fun applyPixelation(bitmap: Bitmap, blockSize: Int = 10): Bitmap {
342
+ val w = bitmap.width
343
+ val h = bitmap.height
344
+
345
+ val smallW = w / blockSize
346
+ val smallH = h / blockSize
347
+
348
+ // Scale down
349
+ val small = Bitmap.createScaledBitmap(bitmap, smallW, smallH, false)
350
+
351
+ // Scale back up with nearest neighbor
352
+ val result = Bitmap.createScaledBitmap(small, w, h, false)
353
+
354
+ small.recycle()
355
+
356
+ return result
357
+ }
358
+
359
+ /**
360
+ * Apply blur to a specific region of the bitmap
361
+ */
362
+ fun blurRegion(
363
+ context: Context,
364
+ bitmap: Bitmap,
365
+ left: Int,
366
+ top: Int,
367
+ right: Int,
368
+ bottom: Int,
369
+ radius: Float = DEFAULT_BLUR_RADIUS
370
+ ): Bitmap {
371
+ val mutableBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
372
+
373
+ val safeLeft = left.coerceIn(0, bitmap.width)
374
+ val safeTop = top.coerceIn(0, bitmap.height)
375
+ val safeRight = right.coerceIn(safeLeft, bitmap.width)
376
+ val safeBottom = bottom.coerceIn(safeTop, bitmap.height)
377
+
378
+ val regionWidth = safeRight - safeLeft
379
+ val regionHeight = safeBottom - safeTop
380
+
381
+ if (regionWidth <= 0 || regionHeight <= 0) return mutableBitmap
382
+
383
+ // Extract region
384
+ val region = Bitmap.createBitmap(bitmap, safeLeft, safeTop, regionWidth, regionHeight)
385
+
386
+ // Blur region
387
+ val blurredRegion = applyGaussianBlur(context, region, radius)
388
+
389
+ // Draw blurred region back
390
+ val canvas = Canvas(mutableBitmap)
391
+ canvas.drawBitmap(blurredRegion, safeLeft.toFloat(), safeTop.toFloat(), null)
392
+
393
+ region.recycle()
394
+ blurredRegion.recycle()
395
+
396
+ return mutableBitmap
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Extension function to blur bitmap
402
+ */
403
+ fun Bitmap.blur(context: Context, radius: Float = 15f): Bitmap {
404
+ return ImageBlur.applyGaussianBlur(context, this, radius)
405
+ }
406
+
407
+ /**
408
+ * Extension function to pixelate bitmap
409
+ */
410
+ fun Bitmap.pixelate(blockSize: Int = 10): Bitmap {
411
+ return ImageBlur.applyPixelation(this, blockSize)
412
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Copyright 2026 Rejourney
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ package com.rejourney.utility
18
+
19
+ import android.view.View
20
+ import android.view.ViewGroup
21
+ import android.widget.EditText
22
+ import android.widget.TextView
23
+
24
+ /**
25
+ * View identification utilities
26
+ * Android implementation aligned with iOS ViewIdentifier.swift
27
+ */
28
+ object ViewIdentifier {
29
+
30
+ /**
31
+ * Generate a stable identifier for a view based on its position in hierarchy
32
+ */
33
+ fun generateStableId(view: View): String {
34
+ val path = mutableListOf<String>()
35
+ var current: View? = view
36
+
37
+ while (current != null) {
38
+ val segment = buildSegment(current)
39
+ path.add(0, segment)
40
+
41
+ current = current.parent as? View
42
+ }
43
+
44
+ return path.joinToString("/")
45
+ }
46
+
47
+ /**
48
+ * Generate a short hash identifier
49
+ */
50
+ fun generateShortId(view: View): String {
51
+ val stableId = generateStableId(view)
52
+ return stableId.hashCode().toUInt().toString(16).take(8)
53
+ }
54
+
55
+ private fun buildSegment(view: View): String {
56
+ val className = view.javaClass.simpleName
57
+ val index = getIndexInParent(view)
58
+
59
+ // Use resource ID if available
60
+ val resourceId = view.id
61
+ if (resourceId != View.NO_ID) {
62
+ try {
63
+ val resourceName = view.resources.getResourceEntryName(resourceId)
64
+ return "$className[$resourceName]"
65
+ } catch (_: Exception) {
66
+ // Resource name not available
67
+ }
68
+ }
69
+
70
+ // Use content description if available
71
+ val contentDesc = view.contentDescription?.toString()
72
+ if (!contentDesc.isNullOrBlank() && contentDesc.length < 32) {
73
+ return "$className[\"$contentDesc\"]"
74
+ }
75
+
76
+ // Use accessibility text for text views
77
+ if (view is TextView) {
78
+ val text = view.text?.toString()?.take(16)
79
+ if (!text.isNullOrBlank()) {
80
+ val sanitized = text.replace(Regex("[^a-zA-Z0-9]"), "_")
81
+ return "$className[\"$sanitized\"]"
82
+ }
83
+ }
84
+
85
+ return "$className[$index]"
86
+ }
87
+
88
+ private fun getIndexInParent(view: View): Int {
89
+ val parent = view.parent as? ViewGroup ?: return 0
90
+
91
+ var index = 0
92
+ val viewClass = view.javaClass
93
+
94
+ for (i in 0 until parent.childCount) {
95
+ val child = parent.getChildAt(i)
96
+ if (child === view) {
97
+ return index
98
+ }
99
+ if (child.javaClass == viewClass) {
100
+ index++
101
+ }
102
+ }
103
+
104
+ return 0
105
+ }
106
+
107
+ /**
108
+ * Find view by stable identifier
109
+ */
110
+ fun findViewByStableId(root: View, stableId: String): View? {
111
+ val segments = stableId.split("/")
112
+ if (segments.isEmpty()) return null
113
+
114
+ var current: View? = root
115
+
116
+ for (segment in segments.drop(1)) {
117
+ current = findChildBySegment(current as? ViewGroup ?: return null, segment)
118
+ if (current == null) return null
119
+ }
120
+
121
+ return current
122
+ }
123
+
124
+ private fun findChildBySegment(parent: ViewGroup, segment: String): View? {
125
+ // Parse segment: ClassName[identifier]
126
+ val match = Regex("(\\w+)\\[(.+)]").find(segment) ?: return null
127
+ val className = match.groupValues[1]
128
+ val identifier = match.groupValues[2]
129
+
130
+ // Try to find by resource ID first
131
+ if (!identifier.startsWith("\"") && !identifier.all { it.isDigit() }) {
132
+ val resourceId = parent.resources.getIdentifier(
133
+ identifier,
134
+ "id",
135
+ parent.context.packageName
136
+ )
137
+ if (resourceId != 0) {
138
+ return parent.findViewById(resourceId)
139
+ }
140
+ }
141
+
142
+ // Find by index
143
+ if (identifier.all { it.isDigit() }) {
144
+ val index = identifier.toIntOrNull() ?: return null
145
+ var count = 0
146
+
147
+ for (i in 0 until parent.childCount) {
148
+ val child = parent.getChildAt(i)
149
+ if (child.javaClass.simpleName == className) {
150
+ if (count == index) {
151
+ return child
152
+ }
153
+ count++
154
+ }
155
+ }
156
+ }
157
+
158
+ return null
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Extension functions for View identification
164
+ */
165
+ val View.rjStableId: String
166
+ get() = ViewIdentifier.generateStableId(this)
167
+
168
+ val View.rjShortId: String
169
+ get() = ViewIdentifier.generateShortId(this)