@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.
- package/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- 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
|
+
}
|