@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,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
|
+
}
|