@rejourneyco/react-native 1.0.0

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 (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. package/src/types/index.ts +739 -0
@@ -0,0 +1,633 @@
1
+ /**
2
+ * Unified view hierarchy scanner aligned with iOS RJViewHierarchyScanner.
3
+ */
4
+ package com.rejourney.capture
5
+
6
+ import android.graphics.Rect
7
+ import android.os.SystemClock
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.view.Window
11
+ import android.widget.EditText
12
+ import android.widget.HorizontalScrollView
13
+ import android.widget.ScrollView
14
+ import android.widget.TextView
15
+ import androidx.core.widget.NestedScrollView
16
+ import androidx.recyclerview.widget.RecyclerView
17
+ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
18
+ import com.rejourney.core.Logger
19
+ import com.rejourney.privacy.PrivacyMask
20
+ import java.util.WeakHashMap
21
+ import kotlin.math.abs
22
+ import kotlin.math.max
23
+ import kotlin.math.min
24
+
25
+ data class ViewHierarchyScanResult(
26
+ val layoutSignature: String? = null,
27
+ val textInputFrames: List<Rect> = emptyList(),
28
+ val cameraFrames: List<Rect> = emptyList(),
29
+ val webViewFrames: List<Rect> = emptyList(),
30
+ val videoFrames: List<Rect> = emptyList(),
31
+ val mapViewFrames: List<Rect> = emptyList(),
32
+ val mapViewPointers: List<View> = emptyList(),
33
+ val scrollViewPointers: List<View> = emptyList(),
34
+ val animatedViewPointers: List<View> = emptyList(),
35
+ val hasMapView: Boolean = false,
36
+ val scrollActive: Boolean = false,
37
+ val bounceActive: Boolean = false,
38
+ val refreshActive: Boolean = false,
39
+ val mapActive: Boolean = false,
40
+ val hasAnyAnimations: Boolean = false,
41
+ val animationAreaRatio: Float = 0f,
42
+ val didBailOutEarly: Boolean = false,
43
+ val totalViewsScanned: Int = 0,
44
+ val scanTimestamp: Long = System.currentTimeMillis()
45
+ )
46
+
47
+ class ViewHierarchyScannerConfig {
48
+ var detectTextInputs: Boolean = true
49
+ var detectCameraViews: Boolean = true
50
+ var detectWebViews: Boolean = true
51
+ var detectVideoLayers: Boolean = true
52
+ var maxDepth: Int = 25
53
+ var maxViewCount: Int = 2000
54
+
55
+ companion object {
56
+ fun defaultConfig(): ViewHierarchyScannerConfig = ViewHierarchyScannerConfig().apply {
57
+ maxDepth = 8
58
+ maxViewCount = 500
59
+ }
60
+ }
61
+ }
62
+
63
+ class ViewHierarchyScanner(val config: ViewHierarchyScannerConfig = ViewHierarchyScannerConfig.defaultConfig()) {
64
+ private val textInputFrames = mutableListOf<Rect>()
65
+ private val cameraFrames = mutableListOf<Rect>()
66
+ private val webViewFrames = mutableListOf<Rect>()
67
+ private val videoFrames = mutableListOf<Rect>()
68
+ private val mapViewFrames = mutableListOf<Rect>()
69
+ private val mapViewPointers = mutableListOf<View>()
70
+ private val scrollViewPointers = mutableListOf<View>()
71
+ private val animatedViewPointers = mutableListOf<View>()
72
+
73
+ private val scrollStateCache = WeakHashMap<View, ScrollState>()
74
+ private val mapStateCache = WeakHashMap<View, MapState>()
75
+
76
+ private var primaryWindowLocation = IntArray(2)
77
+ private var primaryBounds = Rect()
78
+
79
+ private var viewCount = 0
80
+ private var didBailOutEarly = false
81
+ private var foundMapView = false
82
+ private var scanScrollActive = false
83
+ private var scanBounceActive = false
84
+ private var scanRefreshActive = false
85
+ private var scanMapActive = false
86
+ private var scanHasAnimations = false
87
+ private var scanAnimatedArea = 0f
88
+ private var scanStartTime = 0L
89
+
90
+ private var layoutSignatureHash = FNV_OFFSET_BASIS
91
+
92
+ private val resolvedCameraClasses = mutableListOf<Class<*>>()
93
+ private val resolvedWebViewClasses = mutableListOf<Class<*>>()
94
+
95
+ fun prewarmClassCaches() {
96
+ resolveClasses(cameraClassNames, resolvedCameraClasses)
97
+ resolveClasses(webViewClassNames, resolvedWebViewClasses)
98
+ Logger.debug("ViewHierarchyScanner: Class caches pre-warmed")
99
+ }
100
+
101
+ fun scanWindow(window: Window?): ViewHierarchyScanResult? {
102
+ val root = window?.decorView ?: return null
103
+ return scanWindows(listOf(root), root)
104
+ }
105
+
106
+ fun scanAllWindowsRelativeTo(primaryWindow: Window?): ViewHierarchyScanResult? {
107
+ val primaryRoot = primaryWindow?.decorView ?: return null
108
+ val allRoots = getAllVisibleWindowRoots(primaryRoot)
109
+ return scanWindows(allRoots, primaryRoot)
110
+ }
111
+
112
+ private fun scanWindows(windowRoots: List<View>, primaryRoot: View): ViewHierarchyScanResult {
113
+ resetState(primaryRoot)
114
+
115
+ for (root in windowRoots) {
116
+ scanView(root, primaryRoot, 0)
117
+ if (needsPrivacyFallback() && (viewCount >= config.maxViewCount || didBailOutEarly)) {
118
+ scanSensitiveViewsOnly(root, primaryRoot)
119
+ }
120
+ }
121
+
122
+ if (didBailOutEarly && needsPrivacyFallback()) {
123
+ for (root in windowRoots) {
124
+ scanSensitiveViewsOnly(root, primaryRoot)
125
+ }
126
+ }
127
+
128
+ val signature = if (layoutSignatureHash != FNV_OFFSET_BASIS) {
129
+ java.lang.Long.toUnsignedString(layoutSignatureHash, 16).padStart(16, '0')
130
+ } else {
131
+ null
132
+ }
133
+
134
+ val screenArea = max(1f, (primaryBounds.width() * primaryBounds.height()).toFloat())
135
+ return ViewHierarchyScanResult(
136
+ layoutSignature = signature,
137
+ textInputFrames = textInputFrames.map { Rect(it) },
138
+ cameraFrames = cameraFrames.map { Rect(it) },
139
+ webViewFrames = webViewFrames.map { Rect(it) },
140
+ videoFrames = videoFrames.map { Rect(it) },
141
+ mapViewFrames = mapViewFrames.map { Rect(it) },
142
+ mapViewPointers = mapViewPointers.toList(),
143
+ scrollViewPointers = scrollViewPointers.toList(),
144
+ animatedViewPointers = animatedViewPointers.toList(),
145
+ hasMapView = foundMapView,
146
+ scrollActive = scanScrollActive,
147
+ bounceActive = scanBounceActive,
148
+ refreshActive = scanRefreshActive,
149
+ mapActive = scanMapActive,
150
+ hasAnyAnimations = scanHasAnimations,
151
+ animationAreaRatio = min(scanAnimatedArea / screenArea, 1f),
152
+ didBailOutEarly = didBailOutEarly,
153
+ totalViewsScanned = viewCount,
154
+ scanTimestamp = System.currentTimeMillis()
155
+ )
156
+ }
157
+
158
+ private fun scanView(view: View, primaryRoot: View, depth: Int) {
159
+ if (viewCount >= config.maxViewCount || depth > config.maxDepth) {
160
+ didBailOutEarly = true
161
+ return
162
+ }
163
+
164
+ if (viewCount > 0 && viewCount % VIEW_TIME_CHECK_INTERVAL == 0) {
165
+ val elapsed = SystemClock.elapsedRealtime() - scanStartTime
166
+ if (elapsed > MAX_SCAN_TIME_MS) {
167
+ didBailOutEarly = true
168
+ return
169
+ }
170
+ }
171
+
172
+ if (!isViewVisible(view) && depth > 0) return
173
+
174
+ viewCount++
175
+
176
+ val isWebView = config.detectWebViews && isWebView(view)
177
+ val isCamera = config.detectCameraViews && isCameraPreview(view)
178
+ val isVideo = config.detectVideoLayers && isVideoLayerView(view)
179
+ val isBlockedSurface = isWebView || isCamera || isVideo
180
+
181
+ checkSensitiveView(view, primaryRoot, isWebView, isCamera, isVideo)
182
+ trackScrollView(view)
183
+
184
+ if (isBlockedSurface) {
185
+ appendBlockedSurfaceInfoToSignature(view, depth)
186
+ return
187
+ }
188
+
189
+ appendViewInfoToSignature(view, depth)
190
+ trackAnimationsForView(view)
191
+
192
+ if (view is ViewGroup) {
193
+ for (i in view.childCount - 1 downTo 0) {
194
+ scanView(view.getChildAt(i), primaryRoot, depth + 1)
195
+ }
196
+ }
197
+ }
198
+
199
+ private fun checkSensitiveView(
200
+ view: View,
201
+ primaryRoot: View,
202
+ isWebView: Boolean,
203
+ isCamera: Boolean,
204
+ isVideo: Boolean
205
+ ) {
206
+ val isTextInput = config.detectTextInputs && isActualTextInput(view)
207
+ val isManuallyMasked = PrivacyMask.isManuallyMasked(view)
208
+ val isMapView = isMapView(view)
209
+
210
+ if (isMapView) {
211
+ foundMapView = true
212
+ if (isMapViewActive(view)) {
213
+ scanMapActive = true
214
+ }
215
+ addFrame(view, primaryRoot, mapViewFrames)
216
+ mapViewPointers.add(view)
217
+ }
218
+
219
+ if (!isTextInput && !isCamera && !isWebView && !isVideo && !isManuallyMasked) {
220
+ return
221
+ }
222
+
223
+ val targetList = when {
224
+ isCamera -> cameraFrames
225
+ isWebView -> webViewFrames
226
+ isVideo -> videoFrames
227
+ else -> textInputFrames
228
+ }
229
+ addFrame(view, primaryRoot, targetList)
230
+ }
231
+
232
+ private fun addFrame(view: View, primaryRoot: View, target: MutableList<Rect>) {
233
+ val location = IntArray(2)
234
+ view.getLocationOnScreen(location)
235
+ val left = location[0] - primaryWindowLocation[0]
236
+ val top = location[1] - primaryWindowLocation[1]
237
+ val right = left + view.width
238
+ val bottom = top + view.height
239
+ val rect = Rect(left, top, right, bottom)
240
+ if (!Rect.intersects(rect, primaryBounds)) return
241
+
242
+ val clipped = Rect(rect)
243
+ clipped.intersect(primaryBounds)
244
+ if (clipped.width() > MIN_FRAME_SIZE && clipped.height() > MIN_FRAME_SIZE) {
245
+ target.add(clipped)
246
+ }
247
+ }
248
+
249
+ private fun scanSensitiveViewsOnly(windowRoot: View, primaryRoot: View) {
250
+ val queue = ArrayDeque<View>()
251
+ queue.add(windowRoot)
252
+ val start = SystemClock.elapsedRealtime()
253
+ var scanned = 0
254
+
255
+ while (queue.isNotEmpty()) {
256
+ if (scanned >= PRIVACY_FALLBACK_MAX_VIEWS) break
257
+ if (SystemClock.elapsedRealtime() - start > PRIVACY_FALLBACK_MAX_MS) break
258
+
259
+ val current = queue.removeFirst()
260
+ if (isViewVisible(current)) {
261
+ val isWebView = config.detectWebViews && isWebView(current)
262
+ val isCamera = config.detectCameraViews && isCameraPreview(current)
263
+ val isVideo = config.detectVideoLayers && isVideoLayerView(current)
264
+ checkSensitiveView(current, primaryRoot, isWebView, isCamera, isVideo)
265
+ }
266
+
267
+ if (current is ViewGroup) {
268
+ for (i in current.childCount - 1 downTo 0) {
269
+ queue.add(current.getChildAt(i))
270
+ }
271
+ }
272
+ scanned++
273
+ }
274
+ }
275
+
276
+ private fun trackScrollView(view: View) {
277
+ when (view) {
278
+ is ScrollView, is HorizontalScrollView, is NestedScrollView, is RecyclerView -> {
279
+ scrollViewPointers.add(view)
280
+ val state = scrollStateCache[view] ?: ScrollState()
281
+ val (offsetX, offsetY, contentWidth, contentHeight) = scrollMetricsFor(view)
282
+
283
+ val offsetMoved = state.initialized &&
284
+ (abs(offsetX - state.offsetX) > SCROLL_EPSILON ||
285
+ abs(offsetY - state.offsetY) > SCROLL_EPSILON)
286
+ if (offsetMoved || (view is RecyclerView && view.scrollState != RecyclerView.SCROLL_STATE_IDLE)) {
287
+ scanScrollActive = true
288
+ }
289
+
290
+ if (isOverscrolling(view, offsetX, offsetY, contentWidth, contentHeight)) {
291
+ scanBounceActive = true
292
+ }
293
+
294
+ val refreshActive = (view as? SwipeRefreshLayout)?.isRefreshing ?: false
295
+ if (refreshActive) {
296
+ scanRefreshActive = true
297
+ }
298
+
299
+ state.offsetX = offsetX
300
+ state.offsetY = offsetY
301
+ state.contentWidth = contentWidth
302
+ state.contentHeight = contentHeight
303
+ state.initialized = true
304
+ scrollStateCache[view] = state
305
+ }
306
+ is SwipeRefreshLayout -> {
307
+ if (view.isRefreshing) {
308
+ scanRefreshActive = true
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ private fun trackAnimationsForView(view: View) {
315
+ val animation = view.animation
316
+ val hasAnimation = (animation != null && animation.hasStarted() && !animation.hasEnded()) || view.hasTransientState()
317
+ if (!hasAnimation) return
318
+
319
+ scanHasAnimations = true
320
+ animatedViewPointers.add(view)
321
+
322
+ val location = IntArray(2)
323
+ view.getLocationOnScreen(location)
324
+ val left = location[0] - primaryWindowLocation[0]
325
+ val top = location[1] - primaryWindowLocation[1]
326
+ val right = left + view.width
327
+ val bottom = top + view.height
328
+ val rect = Rect(left, top, right, bottom)
329
+ if (!Rect.intersects(rect, primaryBounds)) return
330
+
331
+ val clipped = Rect(rect)
332
+ clipped.intersect(primaryBounds)
333
+ scanAnimatedArea += clipped.width() * clipped.height()
334
+ }
335
+
336
+ private fun appendViewInfoToSignature(view: View, depth: Int) {
337
+ mixInt(depth)
338
+ mixInt(System.identityHashCode(view.javaClass))
339
+
340
+ mixInt(view.left)
341
+ mixInt(view.top)
342
+ mixInt(view.width)
343
+ mixInt(view.height)
344
+
345
+ if (view is ScrollView || view is HorizontalScrollView || view is NestedScrollView || view is RecyclerView) {
346
+ val (offsetX, offsetY, _, _) = scrollMetricsFor(view)
347
+ mixInt(offsetX)
348
+ mixInt(offsetY)
349
+ }
350
+
351
+ if (view is TextView) {
352
+ val text = view.text?.toString().orEmpty()
353
+ if (view is EditText) {
354
+ mixInt(text.length)
355
+ } else if (text.isNotEmpty()) {
356
+ mixInt(text.hashCode())
357
+ }
358
+ }
359
+
360
+ val label = view.contentDescription?.toString()
361
+ if (!label.isNullOrEmpty()) {
362
+ mixInt(label.hashCode())
363
+ }
364
+
365
+ if (view is android.widget.ImageView) {
366
+ view.drawable?.let { mixInt(System.identityHashCode(it)) }
367
+ }
368
+
369
+ val background = view.background
370
+ if (background != null) {
371
+ mixInt(background.hashCode())
372
+ }
373
+
374
+ mixInt((view.alpha * 100).toInt())
375
+ mixInt(if (view.visibility == View.VISIBLE) 0 else 1)
376
+ }
377
+
378
+ private fun appendBlockedSurfaceInfoToSignature(view: View, depth: Int) {
379
+ mixInt(depth)
380
+ mixInt(System.identityHashCode(view.javaClass))
381
+
382
+ mixInt(view.left)
383
+ mixInt(view.top)
384
+ mixInt(view.width)
385
+ mixInt(view.height)
386
+
387
+ mixInt((view.alpha * 100).toInt())
388
+ mixInt(if (view.visibility == View.VISIBLE) 0 else 1)
389
+ }
390
+
391
+ private fun isOverscrolling(view: View, offsetX: Int, offsetY: Int, contentWidth: Int, contentHeight: Int): Boolean {
392
+ return when (view) {
393
+ is ScrollView, is NestedScrollView -> {
394
+ val maxY = max(0, contentHeight - view.height).toFloat()
395
+ offsetY.toFloat() < -SCROLL_EPSILON || offsetY.toFloat() > maxY + SCROLL_EPSILON
396
+ }
397
+ is HorizontalScrollView -> {
398
+ val maxX = max(0, contentWidth - view.width).toFloat()
399
+ offsetX.toFloat() < -SCROLL_EPSILON || offsetX.toFloat() > maxX + SCROLL_EPSILON
400
+ }
401
+ else -> false
402
+ }
403
+ }
404
+
405
+ private fun scrollMetricsFor(view: View): ScrollMetrics {
406
+ return when (view) {
407
+ is RecyclerView -> ScrollMetrics(
408
+ view.computeHorizontalScrollOffset(),
409
+ view.computeVerticalScrollOffset(),
410
+ view.computeHorizontalScrollRange(),
411
+ view.computeVerticalScrollRange()
412
+ )
413
+ is ScrollView -> ScrollMetrics(
414
+ view.scrollX,
415
+ view.scrollY,
416
+ view.getChildAt(0)?.width ?: view.width,
417
+ view.getChildAt(0)?.height ?: view.height
418
+ )
419
+ is NestedScrollView -> ScrollMetrics(
420
+ view.scrollX,
421
+ view.scrollY,
422
+ view.getChildAt(0)?.width ?: view.width,
423
+ view.getChildAt(0)?.height ?: view.height
424
+ )
425
+ is HorizontalScrollView -> ScrollMetrics(
426
+ view.scrollX,
427
+ view.scrollY,
428
+ view.getChildAt(0)?.width ?: view.width,
429
+ view.getChildAt(0)?.height ?: view.height
430
+ )
431
+ else -> ScrollMetrics(view.scrollX, view.scrollY, view.width, view.height)
432
+ }
433
+ }
434
+
435
+ private fun isActualTextInput(view: View): Boolean {
436
+ if (view is EditText) return true
437
+ val className = view.javaClass.simpleName
438
+ return className.contains("TextInput") ||
439
+ className.contains("TextField") ||
440
+ className.contains("EditText")
441
+ }
442
+
443
+ private fun isCameraPreview(view: View): Boolean {
444
+ for (cls in resolvedCameraClasses) {
445
+ if (cls.isInstance(view)) return true
446
+ }
447
+ val className = view.javaClass.simpleName
448
+ return className.contains("Camera") || className.contains("Preview") || className.contains("Scanner")
449
+ }
450
+
451
+ private fun isWebView(view: View): Boolean {
452
+ for (cls in resolvedWebViewClasses) {
453
+ if (cls.isInstance(view)) return true
454
+ }
455
+ val className = view.javaClass.simpleName
456
+ return className.contains("WebView") || className.contains("WKContentView")
457
+ }
458
+
459
+ private fun isVideoLayerView(view: View): Boolean {
460
+ val className = view.javaClass.simpleName
461
+ return className.contains("Video") || className.contains("PlayerView")
462
+ }
463
+
464
+ private fun isMapView(view: View): Boolean {
465
+ val className = view.javaClass.simpleName
466
+ return mapViewClassNames.any { className.contains(it) }
467
+ }
468
+
469
+ private fun isMapViewActive(view: View): Boolean {
470
+ val state = mapStateCache[view] ?: MapState()
471
+ val drawingTime = view.drawingTime
472
+ val animation = view.animation
473
+ val hasAnimation = animation != null && animation.hasStarted() && !animation.hasEnded()
474
+ val hasTransient = view.hasTransientState()
475
+ val isPressed = view.isPressed
476
+ val drawingChanged = state.initialized && drawingTime != state.drawingTime
477
+
478
+ state.drawingTime = drawingTime
479
+ state.initialized = true
480
+ mapStateCache[view] = state
481
+
482
+ return hasAnimation || hasTransient || isPressed || drawingChanged
483
+ }
484
+
485
+ private fun needsPrivacyFallback(): Boolean {
486
+ return (config.detectTextInputs && textInputFrames.isEmpty()) ||
487
+ (config.detectCameraViews && cameraFrames.isEmpty()) ||
488
+ (config.detectWebViews && webViewFrames.isEmpty()) ||
489
+ (config.detectVideoLayers && videoFrames.isEmpty())
490
+ }
491
+
492
+ private fun isViewVisible(view: View): Boolean {
493
+ return view.visibility == View.VISIBLE && view.alpha > 0.01f && view.width > 0 && view.height > 0
494
+ }
495
+
496
+ private fun resetState(primaryRoot: View) {
497
+ textInputFrames.clear()
498
+ cameraFrames.clear()
499
+ webViewFrames.clear()
500
+ videoFrames.clear()
501
+ mapViewFrames.clear()
502
+ mapViewPointers.clear()
503
+ scrollViewPointers.clear()
504
+ animatedViewPointers.clear()
505
+
506
+ viewCount = 0
507
+ didBailOutEarly = false
508
+ foundMapView = false
509
+ scanScrollActive = false
510
+ scanBounceActive = false
511
+ scanRefreshActive = false
512
+ scanMapActive = false
513
+ scanHasAnimations = false
514
+ scanAnimatedArea = 0f
515
+ scanStartTime = SystemClock.elapsedRealtime()
516
+ layoutSignatureHash = FNV_OFFSET_BASIS
517
+
518
+ primaryRoot.getLocationOnScreen(primaryWindowLocation)
519
+ primaryBounds = Rect(0, 0, primaryRoot.width, primaryRoot.height)
520
+ }
521
+
522
+ private fun mixInt(value: Int) {
523
+ layoutSignatureHash = fnv1a(layoutSignatureHash, value.toLong(), INT_BYTES)
524
+ }
525
+
526
+ private fun fnv1a(hash: Long, value: Long, bytes: Int): Long {
527
+ var h = hash
528
+ var v = value
529
+ repeat(bytes) {
530
+ val byte = (v and 0xFF).toInt()
531
+ h = (h xor byte.toLong()) * FNV_PRIME
532
+ v = v ushr 8
533
+ }
534
+ return h
535
+ }
536
+
537
+ private fun resolveClasses(names: List<String>, target: MutableList<Class<*>>) {
538
+ target.clear()
539
+ for (name in names) {
540
+ try {
541
+ target.add(Class.forName(name))
542
+ } catch (_: Exception) {
543
+ }
544
+ }
545
+ }
546
+
547
+ private fun getAllVisibleWindowRoots(primaryRoot: View): List<View> {
548
+ val roots = mutableListOf<View>()
549
+ roots.add(primaryRoot)
550
+ try {
551
+ val wmgClass = Class.forName("android.view.WindowManagerGlobal")
552
+ val wmgInstance = wmgClass.getMethod("getInstance").invoke(null)
553
+ val viewsField = wmgClass.getDeclaredField("mViews")
554
+ viewsField.isAccessible = true
555
+ val views = viewsField.get(wmgInstance)
556
+ when (views) {
557
+ is ArrayList<*> -> {
558
+ for (view in views) {
559
+ if (view is View && view.isShown && view.visibility == View.VISIBLE) {
560
+ if (view != primaryRoot) roots.add(view)
561
+ }
562
+ }
563
+ }
564
+ is Array<*> -> {
565
+ for (view in views) {
566
+ if (view is View && view.isShown && view.visibility == View.VISIBLE) {
567
+ if (view != primaryRoot) roots.add(view)
568
+ }
569
+ }
570
+ }
571
+ }
572
+ } catch (_: Exception) {
573
+ // Best-effort only
574
+ }
575
+ return roots
576
+ }
577
+
578
+ private data class ScrollState(
579
+ var offsetX: Int = 0,
580
+ var offsetY: Int = 0,
581
+ var contentWidth: Int = 0,
582
+ var contentHeight: Int = 0,
583
+ var initialized: Boolean = false
584
+ )
585
+
586
+ private data class ScrollMetrics(
587
+ val offsetX: Int,
588
+ val offsetY: Int,
589
+ val contentWidth: Int,
590
+ val contentHeight: Int
591
+ )
592
+
593
+ private data class MapState(
594
+ var drawingTime: Long = 0L,
595
+ var initialized: Boolean = false
596
+ )
597
+
598
+ companion object {
599
+ private const val MAX_SCAN_TIME_MS = 30L
600
+ private const val VIEW_TIME_CHECK_INTERVAL = 200
601
+ private const val PRIVACY_FALLBACK_MAX_MS = 10L
602
+ private const val PRIVACY_FALLBACK_MAX_VIEWS = 2000
603
+ private const val MIN_FRAME_SIZE = 10
604
+ private const val SCROLL_EPSILON = 1f
605
+
606
+ private const val FNV_OFFSET_BASIS = -3750763034362895579L
607
+ private const val FNV_PRIME = 1099511628211L
608
+ private const val INT_BYTES = 4
609
+
610
+ private val cameraClassNames = listOf(
611
+ "androidx.camera.view.PreviewView",
612
+ "com.google.android.cameraview.CameraView",
613
+ "org.reactnative.camera.RNCameraView",
614
+ "com.mrousavy.camera.CameraView",
615
+ "com.mrousavy.camera.RNCameraView",
616
+ "expo.modules.camera.CameraView"
617
+ )
618
+
619
+ private val webViewClassNames = listOf(
620
+ "android.webkit.WebView",
621
+ "com.reactnativecommunity.webview.RNCWebView",
622
+ "com.facebook.react.views.webview.ReactWebView"
623
+ )
624
+
625
+ private val mapViewClassNames = listOf(
626
+ "MapView",
627
+ "AIRMap",
628
+ "AIRMapView",
629
+ "RNMMapView",
630
+ "GMSMapView"
631
+ )
632
+ }
633
+ }