@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,645 @@
1
+ /**
2
+ * Privacy masking for sensitive view content.
3
+ * Ported from iOS RJPrivacyMask.
4
+ */
5
+ package com.rejourney.privacy
6
+
7
+ import android.app.Activity
8
+ import android.content.Context
9
+ import android.graphics.Bitmap
10
+ import android.graphics.Canvas
11
+ import android.graphics.Color
12
+ import android.graphics.Paint
13
+ import android.graphics.Rect
14
+ import android.graphics.Typeface
15
+ import android.os.Build
16
+ import android.view.View
17
+ import android.view.ViewGroup
18
+ import android.view.Window
19
+ import android.view.WindowManager
20
+ import android.widget.EditText
21
+ import android.widget.PopupWindow
22
+ import com.facebook.react.bridge.ReactApplicationContext
23
+ import com.rejourney.core.Logger
24
+
25
+ object PrivacyMask {
26
+
27
+ var maskTextInputs: Boolean = true
28
+ var maskCameraViews: Boolean = true
29
+ var maskWebViews: Boolean = true
30
+ var maskVideoLayers: Boolean = true
31
+
32
+ private val maskPaint = Paint().apply {
33
+ color = Color.BLACK // Solid black mask
34
+ style = Paint.Style.FILL
35
+ }
36
+
37
+ private val textPaint = Paint().apply {
38
+ color = Color.WHITE
39
+ textSize = 32f // Visible size on standard density
40
+ textAlign = Paint.Align.CENTER
41
+ typeface = Typeface.DEFAULT_BOLD
42
+ isAntiAlias = true
43
+ }
44
+
45
+ // Set of nativeIDs that should be manually masked
46
+ private val maskedNativeIDs = mutableSetOf<String>()
47
+
48
+ /**
49
+ * Add a nativeID to the manually masked set.
50
+ */
51
+ fun addMaskedNativeID(nativeID: String) {
52
+ if (nativeID.isNotEmpty()) {
53
+ maskedNativeIDs.add(nativeID)
54
+ Logger.debug("PrivacyMask: Added masked nativeID: $nativeID")
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Remove a nativeID from the manually masked set.
60
+ */
61
+ fun removeMaskedNativeID(nativeID: String) {
62
+ if (nativeID.isNotEmpty()) {
63
+ maskedNativeIDs.remove(nativeID)
64
+ Logger.debug("PrivacyMask: Removed masked nativeID: $nativeID")
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Apply privacy masking to a captured bitmap.
70
+ * Detects and masks sensitive views like text inputs.
71
+ *
72
+ * @param bitmap The original screen capture
73
+ * @param context Application context
74
+ * @return Masked bitmap (may be the same object if no masking needed)
75
+ */
76
+ fun applyMask(bitmap: Bitmap, context: Context): Bitmap {
77
+ val reactContext = context as? ReactApplicationContext ?: return bitmap
78
+ val rootView = reactContext.currentActivity?.window?.decorView ?: return bitmap
79
+
80
+ val sensitiveRects = findSensitiveRects(rootView)
81
+
82
+ return applyMasksToBitmap(bitmap, sensitiveRects, rootView.width, rootView.height)
83
+ }
84
+
85
+ /**
86
+ * Apply masks to bitmap using pre-calculated rects.
87
+ * Can be run on background thread.
88
+ */
89
+ fun applyMasksToBitmap(bitmap: Bitmap, sensitiveRects: List<Rect>, rootWidth: Int, rootHeight: Int): Bitmap {
90
+ if (sensitiveRects.isEmpty()) return bitmap
91
+
92
+ return try {
93
+ // Create mutable copy if needed - use ARGB_8888 as fallback if config is null
94
+ val config = bitmap.config ?: Bitmap.Config.ARGB_8888
95
+ val mutableBitmap = if (bitmap.isMutable) bitmap else bitmap.copy(config, true)
96
+ val canvas = Canvas(mutableBitmap)
97
+
98
+ // Calculate scale factor
99
+ val scaleX = mutableBitmap.width.toFloat() / rootWidth
100
+ val scaleY = mutableBitmap.height.toFloat() / rootHeight
101
+
102
+ // Draw masks over sensitive areas
103
+ for (rect in sensitiveRects) {
104
+ val scaledRect = Rect(
105
+ (rect.left * scaleX).toInt(),
106
+ (rect.top * scaleY).toInt(),
107
+ (rect.right * scaleX).toInt(),
108
+ (rect.bottom * scaleY).toInt()
109
+ )
110
+ canvas.drawRect(scaledRect, maskPaint)
111
+
112
+ // Draw stars centered
113
+ val centerX = scaledRect.centerX().toFloat()
114
+ // Approximated vertical centering
115
+ val centerY = scaledRect.centerY().toFloat() + (textPaint.textSize / 3)
116
+ canvas.drawText("********", centerX, centerY, textPaint)
117
+ }
118
+
119
+ mutableBitmap
120
+ } catch (e: Exception) {
121
+ Logger.error("Failed to apply privacy mask", e)
122
+ bitmap
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Find all sensitive views and return their screen coordinates.
128
+ * Must be run on Main Thread.
129
+ */
130
+ fun findSensitiveRects(rootView: View): List<Rect> {
131
+ val rects = mutableListOf<Rect>()
132
+
133
+ // IMPORTANT: The capture bitmap is in the Activity window/decorView coordinate space,
134
+ // but View.getLocationOnScreen() returns absolute screen coordinates.
135
+ // Convert to coordinates relative to the provided rootView to ensure masks land correctly.
136
+ val rootLocationOnScreen = IntArray(2)
137
+ rootView.getLocationOnScreen(rootLocationOnScreen)
138
+
139
+ findSensitiveViewsRecursive(
140
+ view = rootView,
141
+ rects = rects,
142
+ rootLocationOnScreen = rootLocationOnScreen,
143
+ rootWidth = rootView.width,
144
+ rootHeight = rootView.height
145
+ )
146
+
147
+ // Fail-closed fallback: if the scan returns nothing but there is a focused sensitive view,
148
+ // at least mask that focused view. This protects against edge cases where parts of the
149
+ // hierarchy are not reachable (e.g., some dialog/modal roots) or transient layout states.
150
+ if (rects.isEmpty()) {
151
+ try {
152
+ val focused = rootView.findFocus()
153
+ if (focused != null && focused.isShown && isSensitiveView(focused) &&
154
+ focused.width > 0 && focused.height > 0
155
+ ) {
156
+ val focusedLoc = IntArray(2)
157
+ focused.getLocationOnScreen(focusedLoc)
158
+
159
+ val left = focusedLoc[0] - rootLocationOnScreen[0]
160
+ val top = focusedLoc[1] - rootLocationOnScreen[1]
161
+ val right = left + focused.width
162
+ val bottom = top + focused.height
163
+
164
+ val clipped = Rect(
165
+ left.coerceAtLeast(0),
166
+ top.coerceAtLeast(0),
167
+ right.coerceAtMost(rootView.width),
168
+ bottom.coerceAtMost(rootView.height)
169
+ )
170
+
171
+ if (!clipped.isEmpty && clipped.width() > 5 && clipped.height() > 5) {
172
+ rects.add(clipped)
173
+ Logger.warning("PrivacyMask: No sensitive rects from scan; masked focused view as fallback")
174
+ }
175
+ }
176
+ } catch (e: Exception) {
177
+ // Best-effort only
178
+ }
179
+ }
180
+ return rects
181
+ }
182
+
183
+ /**
184
+ * Find all sensitive views across ALL visible windows.
185
+ * This includes the main Activity window plus any dialogs, modals, or popup windows.
186
+ *
187
+ * CRITICAL: React Native modals/sheets may create views in separate windows
188
+ * that are not children of the main decorView. This method scans all windows
189
+ * to ensure text inputs in modals are properly masked.
190
+ *
191
+ * Must be run on Main Thread.
192
+ *
193
+ * @param activity The current Activity (to access window and system windows)
194
+ * @param primaryRootView The primary decorView for coordinate reference
195
+ * @return List of sensitive view rects in primaryRootView coordinate space
196
+ */
197
+ fun findSensitiveRectsInAllWindows(activity: Activity, primaryRootView: View): List<Rect> {
198
+ val rects = mutableListOf<Rect>()
199
+ val scannedViews = mutableSetOf<View>()
200
+ var didBailOutEarly = false
201
+
202
+ // Get primary window location for coordinate conversion
203
+ val primaryLocationOnScreen = IntArray(2)
204
+ primaryRootView.getLocationOnScreen(primaryLocationOnScreen)
205
+
206
+ try {
207
+ // 1. Scan the primary decorView (main window)
208
+ findSensitiveViewsRecursive(
209
+ view = primaryRootView,
210
+ rects = rects,
211
+ rootLocationOnScreen = primaryLocationOnScreen,
212
+ rootWidth = primaryRootView.width,
213
+ rootHeight = primaryRootView.height
214
+ )
215
+ scannedViews.add(primaryRootView)
216
+
217
+ // 2. Access all visible windows using reflection on WindowManager
218
+ // This catches Dialogs, PopupWindows, and React Native modals
219
+ val additionalWindows = getAllVisibleWindowRoots(activity)
220
+
221
+ for (windowRoot in additionalWindows) {
222
+ if (windowRoot == primaryRootView) continue
223
+ if (scannedViews.contains(windowRoot)) continue
224
+ if (!windowRoot.isShown) continue
225
+ if (windowRoot.width <= 0 || windowRoot.height <= 0) continue
226
+
227
+ scannedViews.add(windowRoot)
228
+
229
+ // Scan this window for sensitive views
230
+ // Convert coordinates relative to the primary window for proper masking
231
+ val hitLimit = findSensitiveViewsInWindowRelativeTo(
232
+ windowRoot = windowRoot,
233
+ rects = rects,
234
+ primaryLocationOnScreen = primaryLocationOnScreen,
235
+ primaryWidth = primaryRootView.width,
236
+ primaryHeight = primaryRootView.height,
237
+ maxViews = 500
238
+ )
239
+ if (hitLimit) {
240
+ didBailOutEarly = true
241
+ }
242
+ }
243
+
244
+ Logger.debug("PrivacyMask: Scanned ${scannedViews.size} windows, found ${rects.size} sensitive views")
245
+
246
+ } catch (e: Exception) {
247
+ Logger.warning("PrivacyMask: Multi-window scan failed: ${e.message}")
248
+ // Fall back to single-window scan if multi-window fails
249
+ if (rects.isEmpty()) {
250
+ return findSensitiveRects(primaryRootView)
251
+ }
252
+ }
253
+
254
+ // If we bailed out early and found nothing, do a targeted fallback
255
+ // with a higher per-window budget to avoid missing inputs on complex screens.
256
+ if (rects.isEmpty() && didBailOutEarly) {
257
+ try {
258
+ for (windowRoot in getAllVisibleWindowRoots(activity)) {
259
+ if (!windowRoot.isShown) continue
260
+ if (windowRoot.width <= 0 || windowRoot.height <= 0) continue
261
+ findSensitiveViewsInWindowRelativeTo(
262
+ windowRoot = windowRoot,
263
+ rects = rects,
264
+ primaryLocationOnScreen = primaryLocationOnScreen,
265
+ primaryWidth = primaryRootView.width,
266
+ primaryHeight = primaryRootView.height,
267
+ maxViews = 2000
268
+ )
269
+ }
270
+ if (rects.isNotEmpty()) {
271
+ Logger.warning("PrivacyMask: Fallback scan recovered ${rects.size} sensitive views after early bailout")
272
+ }
273
+ } catch (_: Exception) {
274
+ // Best-effort only
275
+ }
276
+ }
277
+
278
+ // Fail-closed fallback for focused view
279
+ if (rects.isEmpty()) {
280
+ try {
281
+ // Check focused view across all windows
282
+ val focusedView = activity.currentFocus
283
+ if (focusedView != null && focusedView.isShown && isSensitiveView(focusedView) &&
284
+ focusedView.width > 0 && focusedView.height > 0
285
+ ) {
286
+ val focusedLoc = IntArray(2)
287
+ focusedView.getLocationOnScreen(focusedLoc)
288
+
289
+ val left = focusedLoc[0] - primaryLocationOnScreen[0]
290
+ val top = focusedLoc[1] - primaryLocationOnScreen[1]
291
+ val right = left + focusedView.width
292
+ val bottom = top + focusedView.height
293
+
294
+ val clipped = Rect(
295
+ left.coerceAtLeast(0),
296
+ top.coerceAtLeast(0),
297
+ right.coerceAtMost(primaryRootView.width),
298
+ bottom.coerceAtMost(primaryRootView.height)
299
+ )
300
+
301
+ if (!clipped.isEmpty && clipped.width() > 5 && clipped.height() > 5) {
302
+ rects.add(clipped)
303
+ Logger.warning("PrivacyMask: Multi-window scan found 0; masked focused view as fallback")
304
+ }
305
+ }
306
+ } catch (e: Exception) {
307
+ // Best-effort only
308
+ }
309
+ }
310
+
311
+ return rects
312
+ }
313
+
314
+ /**
315
+ * Get all visible window roots in the application.
316
+ * Uses reflection to access WindowManager's internal views list.
317
+ */
318
+ @Suppress("UNCHECKED_CAST")
319
+ private fun getAllVisibleWindowRoots(activity: Activity): List<View> {
320
+ val roots = mutableListOf<View>()
321
+
322
+ try {
323
+ // Method 1: Use WindowManager's internal mViews array via reflection
324
+ // This is how system tools and accessibility services access all windows
325
+ val wmgClass = Class.forName("android.view.WindowManagerGlobal")
326
+ val wmgInstance = wmgClass.getMethod("getInstance").invoke(null)
327
+
328
+ // Try to get mViews field (array of DecorViews for all windows)
329
+ val viewsField = wmgClass.getDeclaredField("mViews")
330
+ viewsField.isAccessible = true
331
+ val views = viewsField.get(wmgInstance)
332
+
333
+ if (views is ArrayList<*>) {
334
+ for (view in views) {
335
+ if (view is View && view.isShown && view.visibility == View.VISIBLE) {
336
+ roots.add(view)
337
+ }
338
+ }
339
+ } else if (views is Array<*>) {
340
+ for (view in views) {
341
+ if (view is View && view.isShown && view.visibility == View.VISIBLE) {
342
+ roots.add(view)
343
+ }
344
+ }
345
+ }
346
+
347
+ Logger.debug("PrivacyMask: Found ${roots.size} window roots via reflection")
348
+
349
+ } catch (e: Exception) {
350
+ Logger.debug("PrivacyMask: Reflection method failed (${e.message}), using fallback")
351
+
352
+ // Method 2: Fallback - just scan the activity's own windows
353
+ // This won't catch all dialogs but is safer
354
+ try {
355
+ activity.window?.decorView?.let { roots.add(it) }
356
+ } catch (_: Exception) {}
357
+ }
358
+
359
+ return roots
360
+ }
361
+
362
+ /**
363
+ * Find sensitive views in a window and convert coordinates relative to primary window.
364
+ */
365
+ private fun findSensitiveViewsInWindowRelativeTo(
366
+ windowRoot: View,
367
+ rects: MutableList<Rect>,
368
+ primaryLocationOnScreen: IntArray,
369
+ primaryWidth: Int,
370
+ primaryHeight: Int,
371
+ maxViews: Int
372
+ ): Boolean {
373
+ val queue = ArrayDeque<View>()
374
+ queue.add(windowRoot)
375
+ var viewsScanned = 0
376
+ var hitLimit = false
377
+
378
+ while (queue.isNotEmpty() && viewsScanned < maxViews) {
379
+ val view = queue.removeFirst()
380
+ viewsScanned++
381
+
382
+ if (!view.isShown) continue
383
+
384
+ if (isSensitiveView(view) && view.width > 0 && view.height > 0) {
385
+ val location = IntArray(2)
386
+ view.getLocationOnScreen(location)
387
+
388
+ // Convert to primary window coordinates
389
+ val left = location[0] - primaryLocationOnScreen[0]
390
+ val top = location[1] - primaryLocationOnScreen[1]
391
+ val right = left + view.width
392
+ val bottom = top + view.height
393
+
394
+ // Clip to primary window bounds
395
+ val clipped = Rect(
396
+ left.coerceAtLeast(0),
397
+ top.coerceAtLeast(0),
398
+ right.coerceAtMost(primaryWidth),
399
+ bottom.coerceAtMost(primaryHeight)
400
+ )
401
+
402
+ if (!clipped.isEmpty && clipped.width() > 5 && clipped.height() > 5) {
403
+ rects.add(clipped)
404
+ }
405
+ }
406
+
407
+ if (view is ViewGroup) {
408
+ for (i in 0 until view.childCount) {
409
+ queue.add(view.getChildAt(i))
410
+ }
411
+ }
412
+ }
413
+
414
+ if (queue.isNotEmpty() && viewsScanned >= maxViews) {
415
+ hitLimit = true
416
+ }
417
+
418
+ return hitLimit
419
+ }
420
+
421
+ private fun findSensitiveViewsRecursive(
422
+ view: View,
423
+ rects: MutableList<Rect>,
424
+ rootLocationOnScreen: IntArray,
425
+ rootWidth: Int,
426
+ rootHeight: Int
427
+ ) {
428
+ if (!view.isShown) return
429
+
430
+ // Check if this is a sensitive view
431
+ if (isSensitiveView(view)) {
432
+ if (view.width > 0 && view.height > 0) {
433
+ val location = IntArray(2)
434
+ view.getLocationOnScreen(location)
435
+
436
+ // Convert absolute screen coords -> rootView-relative coords
437
+ val left = location[0] - rootLocationOnScreen[0]
438
+ val top = location[1] - rootLocationOnScreen[1]
439
+ val right = left + view.width
440
+ val bottom = top + view.height
441
+
442
+ // Clip to root bounds to avoid huge/offscreen rects
443
+ val clipped = Rect(
444
+ left.coerceAtLeast(0),
445
+ top.coerceAtLeast(0),
446
+ right.coerceAtMost(rootWidth),
447
+ bottom.coerceAtMost(rootHeight)
448
+ )
449
+
450
+ if (!clipped.isEmpty && clipped.width() > 5 && clipped.height() > 5) {
451
+ rects.add(clipped)
452
+ }
453
+ }
454
+ }
455
+
456
+ // Recurse into children
457
+ if (view is ViewGroup) {
458
+ for (i in 0 until view.childCount) {
459
+ findSensitiveViewsRecursive(
460
+ view = view.getChildAt(i),
461
+ rects = rects,
462
+ rootLocationOnScreen = rootLocationOnScreen,
463
+ rootWidth = rootWidth,
464
+ rootHeight = rootHeight
465
+ )
466
+ }
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Determine if a view should be masked.
472
+ */
473
+ internal fun isSensitiveView(view: View): Boolean {
474
+ // Check for manual masking tag first
475
+ if (hasPrivacyTag(view)) return true
476
+
477
+ // Check if view's nativeID is in the masked set
478
+ val viewNativeID = view.getTag(com.facebook.react.R.id.view_tag_native_id)
479
+ if (viewNativeID is String && maskedNativeIDs.contains(viewNativeID)) {
480
+ Logger.debug("PrivacyMask: Found masked nativeID: $viewNativeID")
481
+ return true
482
+ }
483
+
484
+ // Check immediate children for nativeID - React Native nests views so
485
+ // the nativeID may be on a child wrapper rather than the parent container
486
+ if (view is ViewGroup) {
487
+ for (i in view.childCount - 1 downTo 0) {
488
+ val child = view.getChildAt(i)
489
+ val childNativeID = child.getTag(com.facebook.react.R.id.view_tag_native_id)
490
+ if (childNativeID is String && maskedNativeIDs.contains(childNativeID)) {
491
+ Logger.debug("PrivacyMask: Found masked nativeID in child: $childNativeID")
492
+ return true
493
+ }
494
+ }
495
+ }
496
+
497
+ // EditText and subclasses (password fields, etc.)
498
+ if (maskTextInputs && view is EditText) return true
499
+
500
+ // Check for WebView (both Android native and React Native)
501
+ if (maskWebViews && isWebViewSurface(view)) return true
502
+ if (maskCameraViews && isCameraPreview(view)) return true
503
+ if (maskVideoLayers && isVideoLayerView(view)) return true
504
+
505
+ val simpleClassName = view.javaClass.simpleName.lowercase()
506
+ return maskTextInputs && (
507
+ simpleClassName.contains("textinput") ||
508
+ simpleClassName.contains("edittext") ||
509
+ simpleClassName.contains("password") ||
510
+ simpleClassName.contains("securetext")
511
+ )
512
+ }
513
+
514
+ internal fun isManuallyMasked(view: View): Boolean {
515
+ if (hasPrivacyTag(view)) return true
516
+
517
+ val viewNativeID = view.getTag(com.facebook.react.R.id.view_tag_native_id)
518
+ if (viewNativeID is String && maskedNativeIDs.contains(viewNativeID)) {
519
+ return true
520
+ }
521
+
522
+ if (view is ViewGroup) {
523
+ for (i in view.childCount - 1 downTo 0) {
524
+ val child = view.getChildAt(i)
525
+ val childNativeID = child.getTag(com.facebook.react.R.id.view_tag_native_id)
526
+ if (childNativeID is String && maskedNativeIDs.contains(childNativeID)) {
527
+ return true
528
+ }
529
+ }
530
+ }
531
+
532
+ return false
533
+ }
534
+
535
+ /**
536
+ * Check if a view has a privacy tag set (for manual marking).
537
+ */
538
+ fun hasPrivacyTag(view: View): Boolean {
539
+ val tag = view.tag
540
+ return tag is String && (tag == "rejourney_occlude" || tag == "privacy_mask")
541
+ }
542
+
543
+ private fun isCameraPreview(view: View): Boolean {
544
+ val className = view.javaClass.name.lowercase()
545
+ val simpleName = view.javaClass.simpleName.lowercase()
546
+ return cameraClassNames.any { className.contains(it) } ||
547
+ simpleName.contains("camera") ||
548
+ simpleName.contains("preview") ||
549
+ simpleName.contains("scanner")
550
+ }
551
+
552
+ private fun isWebViewSurface(view: View): Boolean {
553
+ val className = view.javaClass.name.lowercase()
554
+ val simpleName = view.javaClass.simpleName.lowercase()
555
+ return webViewClassNames.any { className.contains(it) } ||
556
+ simpleName.contains("webview") ||
557
+ simpleName.contains("rnwebview") ||
558
+ simpleName.contains("rctwebview")
559
+ }
560
+
561
+ private fun isVideoLayerView(view: View): Boolean {
562
+ val simpleName = view.javaClass.simpleName.lowercase()
563
+ return simpleName.contains("video") || simpleName.contains("playerview")
564
+ }
565
+
566
+ /**
567
+ * Mark a view as sensitive (should be occluded in recordings).
568
+ */
569
+ fun markViewAsSensitive(view: View) {
570
+ view.tag = "rejourney_occlude"
571
+ }
572
+
573
+ /**
574
+ * Unmark a view as sensitive.
575
+ */
576
+ fun unmarkViewAsSensitive(view: View) {
577
+ if (view.tag == "rejourney_occlude") {
578
+ view.tag = null
579
+ }
580
+ }
581
+
582
+ // Class cache for prewarm (matching iOS prewarmClassCaches)
583
+ @Volatile
584
+ private var classesPrewarmed = false
585
+
586
+ /**
587
+ * Pre-warm class lookups to eliminate first-scan cold-cache penalty.
588
+ * This front-loads ~10-15ms of JVM class loading and reflection costs
589
+ * that would otherwise spike on the first frame capture.
590
+ *
591
+ * Matches iOS RJViewHierarchyScanner.prewarmClassCaches behavior.
592
+ */
593
+ fun prewarmClassCaches() {
594
+ if (classesPrewarmed) return
595
+ classesPrewarmed = true
596
+
597
+ try {
598
+ // Force class loading for common sensitive view types
599
+ // These class lookups are cached by the JVM after first access
600
+ EditText::class.java
601
+ android.widget.TextView::class.java
602
+ android.widget.Button::class.java
603
+ ViewGroup::class.java
604
+
605
+ // Force loading of WebView class name strings (used in isSensitiveView)
606
+ val dummyClassNames = listOf(
607
+ "android.webkit.webview",
608
+ "webview",
609
+ "rctwebview",
610
+ "rnwebview",
611
+ "textinput",
612
+ "edittext",
613
+ "password",
614
+ "securetext"
615
+ )
616
+ // Touch each string to ensure interning
617
+ dummyClassNames.forEach { it.lowercase() }
618
+
619
+ // Pre-load React Native tag ID lookup (if available)
620
+ try {
621
+ com.facebook.react.R.id.view_tag_native_id
622
+ } catch (_: Exception) {
623
+ // May not be available in all configurations
624
+ }
625
+
626
+ Logger.debug("PrivacyMask: Class caches pre-warmed")
627
+ } catch (e: Exception) {
628
+ Logger.debug("PrivacyMask: Prewarm warning: ${e.message}")
629
+ }
630
+ }
631
+
632
+ private val cameraClassNames = listOf(
633
+ "androidx.camera.view.previewview",
634
+ "com.google.android.cameraview.cameraview",
635
+ "org.reactnative.camera.rncameraview",
636
+ "com.mrousavy.camera.cameraview",
637
+ "expo.modules.camera.cameraview"
638
+ )
639
+
640
+ private val webViewClassNames = listOf(
641
+ "android.webkit.webview",
642
+ "com.reactnativecommunity.webview.rncwebview",
643
+ "com.facebook.react.views.webview.reactwebview"
644
+ )
645
+ }