@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,1464 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJViewHierarchyScanner.m
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Unified view hierarchy scanner implementation.
|
|
6
|
+
// Combines layout signature generation and privacy rect detection
|
|
7
|
+
// into a single traversal pass for optimal performance.
|
|
8
|
+
//
|
|
9
|
+
// Licensed under the Apache License, Version 2.0 (the "License").
|
|
10
|
+
// Copyright (c) 2026 Rejourney
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
#import "RJViewHierarchyScanner.h"
|
|
14
|
+
#import "../Core/RJLogger.h"
|
|
15
|
+
#import <AVFoundation/AVFoundation.h>
|
|
16
|
+
#import <CommonCrypto/CommonDigest.h>
|
|
17
|
+
#import <UIKit/UIKit.h>
|
|
18
|
+
#import <WebKit/WebKit.h>
|
|
19
|
+
#import <objc/message.h>
|
|
20
|
+
|
|
21
|
+
static const CGFloat kRJScrollEpsilon = 0.5;
|
|
22
|
+
static const CGFloat kRJZoomEpsilon = 0.01;
|
|
23
|
+
static const CGFloat kRJInsetEpsilon = 0.5;
|
|
24
|
+
static const CGFloat kRJAnimationPresentationEpsilon = 1.0;
|
|
25
|
+
|
|
26
|
+
typedef struct {
|
|
27
|
+
double latitude;
|
|
28
|
+
double longitude;
|
|
29
|
+
} RJCoordinate;
|
|
30
|
+
|
|
31
|
+
typedef struct {
|
|
32
|
+
double latitudeDelta;
|
|
33
|
+
double longitudeDelta;
|
|
34
|
+
} RJCoordinateSpan;
|
|
35
|
+
|
|
36
|
+
static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
|
|
37
|
+
const uint8_t *p = data;
|
|
38
|
+
while (len--) {
|
|
39
|
+
h ^= *p++;
|
|
40
|
+
h *= 1099511628211ULL;
|
|
41
|
+
}
|
|
42
|
+
return h;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#pragma mark - Scan Result Implementation
|
|
46
|
+
|
|
47
|
+
@implementation RJViewHierarchyScanResult
|
|
48
|
+
|
|
49
|
+
- (instancetype)init {
|
|
50
|
+
self = [super init];
|
|
51
|
+
if (self) {
|
|
52
|
+
_textInputFrames = @[];
|
|
53
|
+
_cameraFrames = @[];
|
|
54
|
+
_webViewFrames = @[];
|
|
55
|
+
_videoFrames = @[];
|
|
56
|
+
_mapViewFrames = @[];
|
|
57
|
+
_mapViewPointers = @[];
|
|
58
|
+
_scrollViewPointers = @[];
|
|
59
|
+
_animatedViewPointers = @[];
|
|
60
|
+
_scrollActive = NO;
|
|
61
|
+
_bounceActive = NO;
|
|
62
|
+
_refreshActive = NO;
|
|
63
|
+
_mapActive = NO;
|
|
64
|
+
_hasAnyAnimations = NO;
|
|
65
|
+
_animationAreaRatio = 0.0;
|
|
66
|
+
_didBailOutEarly = NO;
|
|
67
|
+
_totalViewsScanned = 0;
|
|
68
|
+
_scanTimestamp = [[NSDate date] timeIntervalSince1970];
|
|
69
|
+
}
|
|
70
|
+
return self;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
- (BOOL)hasWebViews {
|
|
74
|
+
return self.webViewFrames.count > 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
- (BOOL)hasTextInputs {
|
|
78
|
+
return self.textInputFrames.count > 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (BOOL)hasCameraViews {
|
|
82
|
+
return self.cameraFrames.count > 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@end
|
|
86
|
+
|
|
87
|
+
@interface RJScrollViewState : NSObject
|
|
88
|
+
|
|
89
|
+
@property(nonatomic, assign) CGPoint contentOffset;
|
|
90
|
+
@property(nonatomic, assign) UIEdgeInsets contentInset;
|
|
91
|
+
@property(nonatomic, assign) CGFloat zoomScale;
|
|
92
|
+
|
|
93
|
+
@end
|
|
94
|
+
|
|
95
|
+
@implementation RJScrollViewState
|
|
96
|
+
@end
|
|
97
|
+
|
|
98
|
+
#pragma mark - Scanner Configuration Implementation
|
|
99
|
+
|
|
100
|
+
@implementation RJViewHierarchyScannerConfig
|
|
101
|
+
|
|
102
|
+
+ (instancetype)defaultConfig {
|
|
103
|
+
RJViewHierarchyScannerConfig *config =
|
|
104
|
+
[[RJViewHierarchyScannerConfig alloc] init];
|
|
105
|
+
config.detectTextInputs = YES;
|
|
106
|
+
config.detectCameraViews = YES;
|
|
107
|
+
config.detectWebViews = YES;
|
|
108
|
+
config.detectVideoLayers = YES;
|
|
109
|
+
config.maskedNativeIDs = [NSSet set];
|
|
110
|
+
config.maxDepth = 8; // Aggressive optimization
|
|
111
|
+
config.maxViewCount = 500;
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
- (instancetype)init {
|
|
116
|
+
self = [super init];
|
|
117
|
+
if (self) {
|
|
118
|
+
_detectTextInputs = YES;
|
|
119
|
+
_detectCameraViews = YES;
|
|
120
|
+
_detectWebViews = YES;
|
|
121
|
+
_detectVideoLayers = YES;
|
|
122
|
+
_maskedNativeIDs = [NSSet set];
|
|
123
|
+
_maxDepth = 25; // Increased scan depth for better fidelity
|
|
124
|
+
_maxViewCount = 2000; // Increased view count limit
|
|
125
|
+
}
|
|
126
|
+
return self;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@end
|
|
130
|
+
|
|
131
|
+
#pragma mark - Private Interface
|
|
132
|
+
|
|
133
|
+
@interface RJViewHierarchyScanner ()
|
|
134
|
+
|
|
135
|
+
@property(nonatomic, strong) NSSet<NSString *> *rnInputClasses;
|
|
136
|
+
|
|
137
|
+
@property(nonatomic, strong) NSSet<NSString *> *cameraClasses;
|
|
138
|
+
@property(nonatomic, strong) NSSet<NSString *> *webViewClasses;
|
|
139
|
+
@property(nonatomic, strong) NSSet<NSString *> *mapViewClasses;
|
|
140
|
+
|
|
141
|
+
@property(nonatomic, assign) BOOL foundMapView;
|
|
142
|
+
|
|
143
|
+
@property(nonatomic, strong) NSArray<Class> *resolvedRnInputClasses;
|
|
144
|
+
@property(nonatomic, strong) NSArray<Class> *resolvedCameraClasses;
|
|
145
|
+
@property(nonatomic, strong) NSArray<Class> *resolvedWebViewClasses;
|
|
146
|
+
|
|
147
|
+
@property(nonatomic, assign) uint64_t layoutSignatureHash;
|
|
148
|
+
|
|
149
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableTextInputFrames;
|
|
150
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableCameraFrames;
|
|
151
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableWebViewFrames;
|
|
152
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableVideoFrames;
|
|
153
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableMapViewFrames;
|
|
154
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableMapViewPointers;
|
|
155
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableScrollViewPointers;
|
|
156
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *mutableAnimatedViewPointers;
|
|
157
|
+
|
|
158
|
+
@property(nonatomic, strong) NSMapTable<Class, NSString *> *classNameCache;
|
|
159
|
+
|
|
160
|
+
@property(nonatomic, assign) Class cachedUITextField;
|
|
161
|
+
@property(nonatomic, assign) Class cachedUITextView;
|
|
162
|
+
@property(nonatomic, assign) Class cachedUISearchBar;
|
|
163
|
+
@property(nonatomic, assign) Class cachedUIControl;
|
|
164
|
+
@property(nonatomic, assign) Class cachedUIImageView;
|
|
165
|
+
@property(nonatomic, assign) Class cachedUILabel;
|
|
166
|
+
@property(nonatomic, assign) Class cachedAVCapturePreviewLayer;
|
|
167
|
+
@property(nonatomic, assign) Class cachedAVPlayerLayer;
|
|
168
|
+
|
|
169
|
+
@property(nonatomic, assign) NSUInteger viewCount;
|
|
170
|
+
|
|
171
|
+
@property(nonatomic, assign) BOOL scanScrollActive;
|
|
172
|
+
@property(nonatomic, assign) BOOL scanBounceActive;
|
|
173
|
+
@property(nonatomic, assign) BOOL scanRefreshActive;
|
|
174
|
+
@property(nonatomic, assign) BOOL scanMapActive;
|
|
175
|
+
@property(nonatomic, assign) BOOL scanHasAnimations;
|
|
176
|
+
@property(nonatomic, assign) CGFloat scanAnimatedArea;
|
|
177
|
+
|
|
178
|
+
@property(nonatomic, strong)
|
|
179
|
+
NSMapTable<UIScrollView *, RJScrollViewState *> *scrollStateCache;
|
|
180
|
+
@property(nonatomic, strong) NSMapTable<UIView *, NSString *> *mapStateCache;
|
|
181
|
+
|
|
182
|
+
/// Whether the main scan bailed out early due to depth, view count, or time
|
|
183
|
+
@property(nonatomic, assign) BOOL didBailOutEarly;
|
|
184
|
+
|
|
185
|
+
/// Timestamp when scan started (for time-based bailout)
|
|
186
|
+
@property(nonatomic, assign) CFAbsoluteTime scanStartTime;
|
|
187
|
+
|
|
188
|
+
/// Maximum scan time in seconds before bailout (default: 30ms)
|
|
189
|
+
@property(nonatomic, assign) CFTimeInterval maxScanTime;
|
|
190
|
+
|
|
191
|
+
/// Primary capture window (for multi-window coordinate conversion)
|
|
192
|
+
/// When set, all sensitive view frames are converted to this window's
|
|
193
|
+
/// coordinate space
|
|
194
|
+
@property(nonatomic, weak) UIWindow *primaryCaptureWindow;
|
|
195
|
+
|
|
196
|
+
- (void)scanView:(UIView *)view
|
|
197
|
+
inWindow:(UIWindow *)window
|
|
198
|
+
depth:(NSInteger)depth;
|
|
199
|
+
- (void)scanSensitiveViewsOnlyInWindow:(UIWindow *)window;
|
|
200
|
+
- (BOOL)isMapViewLoading:(UIView *)view;
|
|
201
|
+
- (BOOL)isMapViewGestureActive:(UIView *)view;
|
|
202
|
+
- (BOOL)hasPresentationDeltaForView:(UIView *)view;
|
|
203
|
+
|
|
204
|
+
@end
|
|
205
|
+
|
|
206
|
+
#pragma mark - Implementation
|
|
207
|
+
|
|
208
|
+
@implementation RJViewHierarchyScanner
|
|
209
|
+
|
|
210
|
+
#pragma mark - Initialization
|
|
211
|
+
|
|
212
|
+
- (instancetype)init {
|
|
213
|
+
return [self initWithConfig:[RJViewHierarchyScannerConfig defaultConfig]];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
- (instancetype)initWithConfig:(RJViewHierarchyScannerConfig *)config {
|
|
217
|
+
self = [super init];
|
|
218
|
+
if (self) {
|
|
219
|
+
_config = config;
|
|
220
|
+
_maxScanTime = 0.030; // 30ms soft cap for scan time
|
|
221
|
+
|
|
222
|
+
_rnInputClasses = [NSSet setWithArray:@[
|
|
223
|
+
@"RCTUITextField",
|
|
224
|
+
@"RCTBaseTextInputView",
|
|
225
|
+
@"RCTSinglelineTextInputView",
|
|
226
|
+
@"RCTMultilineTextInputView",
|
|
227
|
+
]];
|
|
228
|
+
|
|
229
|
+
_cameraClasses = [NSSet setWithArray:@[
|
|
230
|
+
@"AVCaptureVideoPreviewView",
|
|
231
|
+
@"CameraView",
|
|
232
|
+
@"CKCameraView",
|
|
233
|
+
@"RNCameraView",
|
|
234
|
+
@"VisionCameraView",
|
|
235
|
+
@"RNVisionCameraView",
|
|
236
|
+
@"ExpoCameraView",
|
|
237
|
+
]];
|
|
238
|
+
|
|
239
|
+
_webViewClasses = [NSSet setWithArray:@[
|
|
240
|
+
@"WKWebView",
|
|
241
|
+
@"UIWebView",
|
|
242
|
+
@"RCTWebView",
|
|
243
|
+
@"RNCWebView",
|
|
244
|
+
@"RNCWKWebView",
|
|
245
|
+
@"RCTWKWebView",
|
|
246
|
+
@"RNWebView",
|
|
247
|
+
@"WKContentView",
|
|
248
|
+
]];
|
|
249
|
+
|
|
250
|
+
// MapView classes - when present, frame caching should be disabled
|
|
251
|
+
// because map tiles load asynchronously and layout signature doesn't
|
|
252
|
+
// capture them
|
|
253
|
+
_mapViewClasses = [NSSet setWithArray:@[
|
|
254
|
+
@"MKMapView", // Apple Maps
|
|
255
|
+
@"AIRMap", // react-native-maps (iOS)
|
|
256
|
+
@"AIRMapView", // react-native-maps alternate
|
|
257
|
+
@"RNMMapView", // react-native-maps newer versions
|
|
258
|
+
@"GMSMapView", // Google Maps SDK
|
|
259
|
+
]];
|
|
260
|
+
|
|
261
|
+
_layoutSignatureHash = 14695981039346656037ULL;
|
|
262
|
+
_mutableTextInputFrames = [NSMutableArray arrayWithCapacity:8];
|
|
263
|
+
_mutableCameraFrames = [NSMutableArray arrayWithCapacity:2];
|
|
264
|
+
_mutableWebViewFrames = [NSMutableArray arrayWithCapacity:2];
|
|
265
|
+
_mutableVideoFrames = [NSMutableArray arrayWithCapacity:2];
|
|
266
|
+
_mutableMapViewFrames = [NSMutableArray arrayWithCapacity:2];
|
|
267
|
+
_mutableMapViewPointers = [NSMutableArray arrayWithCapacity:2];
|
|
268
|
+
_mutableScrollViewPointers = [NSMutableArray arrayWithCapacity:4];
|
|
269
|
+
_mutableAnimatedViewPointers = [NSMutableArray arrayWithCapacity:4];
|
|
270
|
+
|
|
271
|
+
_classNameCache = [NSMapTable strongToStrongObjectsMapTable];
|
|
272
|
+
_cachedUITextField = [UITextField class];
|
|
273
|
+
_cachedUITextView = [UITextView class];
|
|
274
|
+
_cachedUISearchBar = [UISearchBar class];
|
|
275
|
+
_cachedUIControl = [UIControl class];
|
|
276
|
+
_cachedUIImageView = [UIImageView class];
|
|
277
|
+
_cachedUILabel = [UILabel class];
|
|
278
|
+
_cachedAVCapturePreviewLayer = [AVCaptureVideoPreviewLayer class];
|
|
279
|
+
_cachedAVPlayerLayer = [AVPlayerLayer class];
|
|
280
|
+
_scrollStateCache = [NSMapTable weakToStrongObjectsMapTable];
|
|
281
|
+
_mapStateCache = [NSMapTable weakToStrongObjectsMapTable];
|
|
282
|
+
}
|
|
283
|
+
return self;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#pragma mark - Public API
|
|
287
|
+
|
|
288
|
+
- (RJViewHierarchyScanResult *)scanWindow:(UIWindow *)window {
|
|
289
|
+
if (!window) {
|
|
290
|
+
return nil;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@try {
|
|
294
|
+
|
|
295
|
+
self.layoutSignatureHash = 14695981039346656037ULL;
|
|
296
|
+
[self.mutableTextInputFrames removeAllObjects];
|
|
297
|
+
[self.mutableCameraFrames removeAllObjects];
|
|
298
|
+
[self.mutableWebViewFrames removeAllObjects];
|
|
299
|
+
[self.mutableVideoFrames removeAllObjects];
|
|
300
|
+
[self.mutableMapViewFrames removeAllObjects];
|
|
301
|
+
[self.mutableMapViewPointers removeAllObjects];
|
|
302
|
+
[self.mutableScrollViewPointers removeAllObjects];
|
|
303
|
+
[self.mutableAnimatedViewPointers removeAllObjects];
|
|
304
|
+
self.viewCount = 0;
|
|
305
|
+
self.foundMapView = NO; // Reset MapView detection
|
|
306
|
+
self.scanScrollActive = NO;
|
|
307
|
+
self.scanBounceActive = NO;
|
|
308
|
+
self.scanRefreshActive = NO;
|
|
309
|
+
self.scanMapActive = NO;
|
|
310
|
+
self.scanHasAnimations = NO;
|
|
311
|
+
self.scanAnimatedArea = 0.0;
|
|
312
|
+
self.primaryCaptureWindow =
|
|
313
|
+
nil; // Clear multi-window state for single-window scan
|
|
314
|
+
self.didBailOutEarly = NO;
|
|
315
|
+
|
|
316
|
+
// Record scan start time for time-based bailout
|
|
317
|
+
self.scanStartTime = CFAbsoluteTimeGetCurrent();
|
|
318
|
+
|
|
319
|
+
[self scanView:window inWindow:window depth:0];
|
|
320
|
+
|
|
321
|
+
// FAIL-CLOSED PRIVACY FALLBACK:
|
|
322
|
+
// On very complex screens, the main traversal may hit maxViewCount/maxDepth
|
|
323
|
+
// before encountering TextInputs, resulting in 0 textInputFrames and thus
|
|
324
|
+
// no masking. If that happens, run a lightweight targeted traversal that
|
|
325
|
+
// ONLY looks for sensitive views (text inputs / camera / manual masks).
|
|
326
|
+
//
|
|
327
|
+
// This keeps the fast path fast, but prevents privacy masking from silently
|
|
328
|
+
// failing on large view trees (e.g., complex RN screens with many
|
|
329
|
+
// subviews).
|
|
330
|
+
BOOL hitViewLimit = (self.viewCount >= self.config.maxViewCount);
|
|
331
|
+
BOOL needsPrivacyFallback =
|
|
332
|
+
(self.config.detectTextInputs &&
|
|
333
|
+
self.mutableTextInputFrames.count == 0) ||
|
|
334
|
+
(self.config.detectCameraViews && self.mutableCameraFrames.count == 0) ||
|
|
335
|
+
(self.config.detectWebViews && self.mutableWebViewFrames.count == 0) ||
|
|
336
|
+
(self.config.detectVideoLayers && self.mutableVideoFrames.count == 0);
|
|
337
|
+
if (needsPrivacyFallback && (hitViewLimit || self.didBailOutEarly)) {
|
|
338
|
+
[self scanSensitiveViewsOnlyInWindow:window];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
RJViewHierarchyScanResult *result =
|
|
342
|
+
[[RJViewHierarchyScanResult alloc] init];
|
|
343
|
+
result.totalViewsScanned = self.viewCount;
|
|
344
|
+
result.textInputFrames = [self.mutableTextInputFrames copy];
|
|
345
|
+
result.cameraFrames = [self.mutableCameraFrames copy];
|
|
346
|
+
result.webViewFrames = [self.mutableWebViewFrames copy];
|
|
347
|
+
result.videoFrames = [self.mutableVideoFrames copy];
|
|
348
|
+
result.hasMapView =
|
|
349
|
+
self.foundMapView; // MapView detection for cache invalidation
|
|
350
|
+
result.mapViewFrames = [self.mutableMapViewFrames copy];
|
|
351
|
+
result.mapViewPointers = [self.mutableMapViewPointers copy];
|
|
352
|
+
result.scrollViewPointers = [self.mutableScrollViewPointers copy];
|
|
353
|
+
result.animatedViewPointers = [self.mutableAnimatedViewPointers copy];
|
|
354
|
+
result.scrollActive = self.scanScrollActive;
|
|
355
|
+
result.bounceActive = self.scanBounceActive;
|
|
356
|
+
result.refreshActive = self.scanRefreshActive;
|
|
357
|
+
result.mapActive = self.scanMapActive;
|
|
358
|
+
result.hasAnyAnimations = self.scanHasAnimations;
|
|
359
|
+
CGFloat screenArea = window.bounds.size.width * window.bounds.size.height;
|
|
360
|
+
result.animationAreaRatio =
|
|
361
|
+
(screenArea > 0)
|
|
362
|
+
? MIN(self.scanAnimatedArea / screenArea, 1.0)
|
|
363
|
+
: 0.0;
|
|
364
|
+
result.didBailOutEarly = self.didBailOutEarly;
|
|
365
|
+
|
|
366
|
+
if (self.layoutSignatureHash != 14695981039346656037ULL) {
|
|
367
|
+
result.layoutSignature =
|
|
368
|
+
[NSString stringWithFormat:@"%016llx", self.layoutSignatureHash];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#ifdef DEBUG
|
|
372
|
+
CFTimeInterval scanDuration =
|
|
373
|
+
CFAbsoluteTimeGetCurrent() - self.scanStartTime;
|
|
374
|
+
if (scanDuration > 0.005) { // Log if scan took > 5ms
|
|
375
|
+
RJLogWarning(@"ViewHierarchyScanner: Slow scan - %lu views in %.1fms",
|
|
376
|
+
(unsigned long)result.totalViewsScanned,
|
|
377
|
+
scanDuration * 1000);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (result.textInputFrames.count > 0 || result.cameraFrames.count > 0) {
|
|
381
|
+
RJLogDebug(
|
|
382
|
+
@"ViewHierarchyScanner: %lu views, %lu text inputs, %lu cameras",
|
|
383
|
+
(unsigned long)result.totalViewsScanned,
|
|
384
|
+
(unsigned long)result.textInputFrames.count,
|
|
385
|
+
(unsigned long)result.cameraFrames.count);
|
|
386
|
+
}
|
|
387
|
+
#endif
|
|
388
|
+
|
|
389
|
+
return result;
|
|
390
|
+
} @catch (NSException *exception) {
|
|
391
|
+
RJLogWarning(@"ViewHierarchyScanner: Scan failed: %@", exception);
|
|
392
|
+
return nil;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
- (nullable RJViewHierarchyScanResult *)scanAllWindowsRelativeTo:
|
|
397
|
+
(UIWindow *)primaryWindow {
|
|
398
|
+
if (!primaryWindow) {
|
|
399
|
+
return nil;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// If no windows provided, scan all connected scenes (expensive fallback)
|
|
403
|
+
NSMutableArray<UIWindow *> *windowsToScan = [NSMutableArray array];
|
|
404
|
+
|
|
405
|
+
if (@available(iOS 13.0, *)) {
|
|
406
|
+
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
|
407
|
+
if (![scene isKindOfClass:[UIWindowScene class]])
|
|
408
|
+
continue;
|
|
409
|
+
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
|
410
|
+
|
|
411
|
+
// Filter out non-active scenes if possible, but be careful with
|
|
412
|
+
// modals
|
|
413
|
+
if (windowScene.activationState == UISceneActivationStateBackground ||
|
|
414
|
+
windowScene.activationState == UISceneActivationStateUnattached) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (UIWindow *window in windowScene.windows) {
|
|
419
|
+
if (!window.isHidden && window.alpha > 0.01) {
|
|
420
|
+
[windowsToScan addObject:window];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
// Fallback for older iOS (unlikely needed but good for safety)
|
|
426
|
+
if (primaryWindow) {
|
|
427
|
+
[windowsToScan addObject:primaryWindow];
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return [self scanWindows:windowsToScan relativeToWindow:primaryWindow];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
- (nullable RJViewHierarchyScanResult *)scanWindows:
|
|
435
|
+
(NSArray<UIWindow *> *)windows
|
|
436
|
+
relativeToWindow:(UIWindow *)primaryWindow {
|
|
437
|
+
if (!primaryWindow || !windows) {
|
|
438
|
+
return nil;
|
|
439
|
+
}
|
|
440
|
+
if (windows.count == 0) {
|
|
441
|
+
return nil;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
@try {
|
|
445
|
+
// Reset state for new frame
|
|
446
|
+
self.viewCount = 0;
|
|
447
|
+
self.layoutSignatureHash = 14695981039346656037ULL; // FNV-1a offset basis
|
|
448
|
+
|
|
449
|
+
// Reset other mutable properties
|
|
450
|
+
[self.mutableTextInputFrames removeAllObjects];
|
|
451
|
+
[self.mutableCameraFrames removeAllObjects];
|
|
452
|
+
[self.mutableWebViewFrames removeAllObjects];
|
|
453
|
+
[self.mutableVideoFrames removeAllObjects];
|
|
454
|
+
[self.mutableMapViewFrames removeAllObjects];
|
|
455
|
+
[self.mutableMapViewPointers removeAllObjects];
|
|
456
|
+
[self.mutableScrollViewPointers removeAllObjects];
|
|
457
|
+
[self.mutableAnimatedViewPointers removeAllObjects];
|
|
458
|
+
self.foundMapView = NO;
|
|
459
|
+
self.scanScrollActive = NO;
|
|
460
|
+
self.scanBounceActive = NO;
|
|
461
|
+
self.scanRefreshActive = NO;
|
|
462
|
+
self.scanMapActive = NO;
|
|
463
|
+
self.scanHasAnimations = NO;
|
|
464
|
+
self.scanAnimatedArea = 0.0;
|
|
465
|
+
self.didBailOutEarly = NO;
|
|
466
|
+
self.primaryCaptureWindow =
|
|
467
|
+
primaryWindow; // Store for coordinate conversion
|
|
468
|
+
|
|
469
|
+
// Record scan start time for time-based bailout
|
|
470
|
+
self.scanStartTime = CFAbsoluteTimeGetCurrent();
|
|
471
|
+
|
|
472
|
+
// Sort windows by level
|
|
473
|
+
NSMutableArray<UIWindow *> *windowsToScan = [windows mutableCopy];
|
|
474
|
+
[windowsToScan
|
|
475
|
+
sortUsingComparator:^NSComparisonResult(UIWindow *w1, UIWindow *w2) {
|
|
476
|
+
if (w1.windowLevel > w2.windowLevel)
|
|
477
|
+
return NSOrderedAscending;
|
|
478
|
+
if (w1.windowLevel < w2.windowLevel)
|
|
479
|
+
return NSOrderedDescending;
|
|
480
|
+
return NSOrderedSame;
|
|
481
|
+
}];
|
|
482
|
+
|
|
483
|
+
RJLogDebug(
|
|
484
|
+
@"ViewHierarchyScanner: Scanning %lu windows for sensitive views",
|
|
485
|
+
(unsigned long)windowsToScan.count);
|
|
486
|
+
|
|
487
|
+
// Scan each window
|
|
488
|
+
for (UIWindow *window in windowsToScan) {
|
|
489
|
+
// Scan this window's view hierarchy
|
|
490
|
+
// checkSensitiveView will use primaryCaptureWindow for coordinate
|
|
491
|
+
// conversion
|
|
492
|
+
[self scanView:window inWindow:window depth:0];
|
|
493
|
+
|
|
494
|
+
// If we're over limits, trigger privacy fallback for this window
|
|
495
|
+
BOOL hitViewLimit = (self.viewCount >= self.config.maxViewCount);
|
|
496
|
+
BOOL needsPrivacyFallback =
|
|
497
|
+
(self.config.detectTextInputs &&
|
|
498
|
+
self.mutableTextInputFrames.count == 0) ||
|
|
499
|
+
(self.config.detectCameraViews &&
|
|
500
|
+
self.mutableCameraFrames.count == 0) ||
|
|
501
|
+
(self.config.detectWebViews && self.mutableWebViewFrames.count == 0) ||
|
|
502
|
+
(self.config.detectVideoLayers &&
|
|
503
|
+
self.mutableVideoFrames.count == 0);
|
|
504
|
+
if (needsPrivacyFallback && hitViewLimit) {
|
|
505
|
+
[self scanSensitiveViewsOnlyInWindow:window];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// If the main scan bailed out early and found no inputs, do a targeted
|
|
510
|
+
// privacy-only scan as a fail-closed fallback.
|
|
511
|
+
BOOL needsPrivacyFallback =
|
|
512
|
+
(self.config.detectTextInputs &&
|
|
513
|
+
self.mutableTextInputFrames.count == 0) ||
|
|
514
|
+
(self.config.detectCameraViews && self.mutableCameraFrames.count == 0) ||
|
|
515
|
+
(self.config.detectWebViews && self.mutableWebViewFrames.count == 0) ||
|
|
516
|
+
(self.config.detectVideoLayers && self.mutableVideoFrames.count == 0);
|
|
517
|
+
if (needsPrivacyFallback && self.didBailOutEarly) {
|
|
518
|
+
for (UIWindow *window in windowsToScan) {
|
|
519
|
+
[self scanSensitiveViewsOnlyInWindow:window];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Build result - frames are already converted to primaryWindow
|
|
524
|
+
// coordinates by checkSensitiveView (using self.primaryCaptureWindow)
|
|
525
|
+
RJViewHierarchyScanResult *result =
|
|
526
|
+
[[RJViewHierarchyScanResult alloc] init];
|
|
527
|
+
result.totalViewsScanned = self.viewCount;
|
|
528
|
+
result.textInputFrames = [self.mutableTextInputFrames copy];
|
|
529
|
+
result.cameraFrames = [self.mutableCameraFrames copy];
|
|
530
|
+
result.webViewFrames = [self.mutableWebViewFrames copy];
|
|
531
|
+
result.videoFrames = [self.mutableVideoFrames copy];
|
|
532
|
+
result.hasMapView = self.foundMapView;
|
|
533
|
+
result.mapViewFrames = [self.mutableMapViewFrames copy];
|
|
534
|
+
result.mapViewPointers = [self.mutableMapViewPointers copy];
|
|
535
|
+
result.scrollViewPointers = [self.mutableScrollViewPointers copy];
|
|
536
|
+
result.animatedViewPointers = [self.mutableAnimatedViewPointers copy];
|
|
537
|
+
result.scrollActive = self.scanScrollActive;
|
|
538
|
+
result.bounceActive = self.scanBounceActive;
|
|
539
|
+
result.refreshActive = self.scanRefreshActive;
|
|
540
|
+
result.mapActive = self.scanMapActive;
|
|
541
|
+
result.hasAnyAnimations = self.scanHasAnimations;
|
|
542
|
+
CGFloat screenArea =
|
|
543
|
+
primaryWindow.bounds.size.width * primaryWindow.bounds.size.height;
|
|
544
|
+
result.animationAreaRatio =
|
|
545
|
+
(screenArea > 0)
|
|
546
|
+
? MIN(self.scanAnimatedArea / screenArea, 1.0)
|
|
547
|
+
: 0.0;
|
|
548
|
+
result.didBailOutEarly = self.didBailOutEarly;
|
|
549
|
+
|
|
550
|
+
if (self.layoutSignatureHash != 14695981039346656037ULL) {
|
|
551
|
+
result.layoutSignature =
|
|
552
|
+
[NSString stringWithFormat:@"%016llx", self.layoutSignatureHash];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Clear primaryCaptureWindow after scan is complete
|
|
556
|
+
self.primaryCaptureWindow = nil;
|
|
557
|
+
|
|
558
|
+
#ifdef DEBUG
|
|
559
|
+
CFTimeInterval scanDuration =
|
|
560
|
+
CFAbsoluteTimeGetCurrent() - self.scanStartTime;
|
|
561
|
+
RJLogDebug(@"ViewHierarchyScanner: Multi-window scan complete - %lu views, "
|
|
562
|
+
@"%lu text inputs, %lu cameras in %.1fms",
|
|
563
|
+
(unsigned long)result.totalViewsScanned,
|
|
564
|
+
(unsigned long)result.textInputFrames.count,
|
|
565
|
+
(unsigned long)result.cameraFrames.count, scanDuration * 1000);
|
|
566
|
+
#endif
|
|
567
|
+
|
|
568
|
+
return result;
|
|
569
|
+
} @catch (NSException *exception) {
|
|
570
|
+
self.primaryCaptureWindow = nil; // Clean up on error
|
|
571
|
+
RJLogWarning(@"ViewHierarchyScanner: Multi-window scan failed: %@",
|
|
572
|
+
exception);
|
|
573
|
+
// Fallback to single-window scan
|
|
574
|
+
return [self scanWindow:primaryWindow];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
- (BOOL)isViewVisible:(UIView *)view {
|
|
579
|
+
return !view.hidden && view.alpha > 0.01 && view.frame.size.width > 0 &&
|
|
580
|
+
view.frame.size.height > 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
- (void)prewarmClassCaches {
|
|
584
|
+
// Pre-warm Objective-C runtime class lookups to eliminate first-scan
|
|
585
|
+
// penalty. NSClassFromString and class hierarchy checks are expensive on
|
|
586
|
+
// first call
|
|
587
|
+
// (~10-15ms total). By doing them here we front-load the cost before
|
|
588
|
+
// capture.
|
|
589
|
+
|
|
590
|
+
// Force resolution of all React Native input classes
|
|
591
|
+
NSMutableArray *rnInputs = [NSMutableArray array];
|
|
592
|
+
for (NSString *className in self.rnInputClasses) {
|
|
593
|
+
Class c = NSClassFromString(className);
|
|
594
|
+
if (c)
|
|
595
|
+
[rnInputs addObject:c];
|
|
596
|
+
}
|
|
597
|
+
self.resolvedRnInputClasses = rnInputs;
|
|
598
|
+
|
|
599
|
+
// Force resolution of all camera/video classes
|
|
600
|
+
NSMutableArray *cameras = [NSMutableArray array];
|
|
601
|
+
for (NSString *className in self.cameraClasses) {
|
|
602
|
+
Class c = NSClassFromString(className);
|
|
603
|
+
if (c)
|
|
604
|
+
[cameras addObject:c];
|
|
605
|
+
}
|
|
606
|
+
self.resolvedCameraClasses = cameras;
|
|
607
|
+
|
|
608
|
+
// Force resolution of all WebView classes
|
|
609
|
+
NSMutableArray *webViews = [NSMutableArray array];
|
|
610
|
+
for (NSString *className in self.webViewClasses) {
|
|
611
|
+
Class c = NSClassFromString(className);
|
|
612
|
+
if (c) {
|
|
613
|
+
[webViews addObject:c];
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
self.resolvedWebViewClasses = webViews;
|
|
617
|
+
|
|
618
|
+
// Pre-warm UIKit class hierarchy checks
|
|
619
|
+
// These trigger internal class resolution and method caching
|
|
620
|
+
UIView *dummyView = [[UIView alloc] init];
|
|
621
|
+
(void)[dummyView isKindOfClass:self.cachedUITextField];
|
|
622
|
+
(void)[dummyView isKindOfClass:self.cachedUITextView];
|
|
623
|
+
(void)[dummyView isKindOfClass:self.cachedUISearchBar];
|
|
624
|
+
(void)[dummyView isKindOfClass:self.cachedUIControl];
|
|
625
|
+
(void)[dummyView isKindOfClass:self.cachedUIImageView];
|
|
626
|
+
(void)[dummyView isKindOfClass:self.cachedUILabel];
|
|
627
|
+
|
|
628
|
+
// Pre-warm CALayer class checks
|
|
629
|
+
CALayer *dummyLayer = [[CALayer alloc] init];
|
|
630
|
+
(void)[dummyLayer isKindOfClass:self.cachedAVCapturePreviewLayer];
|
|
631
|
+
(void)[dummyLayer isKindOfClass:self.cachedAVPlayerLayer];
|
|
632
|
+
|
|
633
|
+
RJLogDebug(@"ViewHierarchyScanner: Class caches pre-warmed");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#pragma mark - Private Traversal
|
|
637
|
+
|
|
638
|
+
- (void)scanView:(UIView *)view
|
|
639
|
+
inWindow:(UIWindow *)window
|
|
640
|
+
depth:(NSInteger)depth {
|
|
641
|
+
if (!view || !window)
|
|
642
|
+
return;
|
|
643
|
+
|
|
644
|
+
// Early exit if we've scanned too many views (prevents runaway on complex
|
|
645
|
+
// UIs)
|
|
646
|
+
if (self.viewCount >= self.config.maxViewCount) {
|
|
647
|
+
self.didBailOutEarly = YES;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (depth > self.config.maxDepth) {
|
|
652
|
+
self.didBailOutEarly = YES;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// TIME-BASED BAILOUT: Check every 200 views if we've exceeded max scan time
|
|
657
|
+
// This is a safety net for truly runaway scans, not normal operation
|
|
658
|
+
if (self.viewCount > 0 && (self.viewCount % 200) == 0) {
|
|
659
|
+
CFTimeInterval elapsed = CFAbsoluteTimeGetCurrent() - self.scanStartTime;
|
|
660
|
+
if (elapsed > self.maxScanTime) {
|
|
661
|
+
self.didBailOutEarly = YES;
|
|
662
|
+
RJLogWarning(@"ViewHierarchyScanner: Time bailout at %lu views (%.1fms > "
|
|
663
|
+
@"%.1fms limit)",
|
|
664
|
+
(unsigned long)self.viewCount, elapsed * 1000,
|
|
665
|
+
self.maxScanTime * 1000);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (![self isViewVisible:view])
|
|
671
|
+
return;
|
|
672
|
+
|
|
673
|
+
// REMOVED: [self.scannedViews containsObject:viewPtr] check
|
|
674
|
+
// Trees are acyclic in UIKit (usually), so we save the set lookups.
|
|
675
|
+
// If we really need cycle detection, restore it, but B2 optimization
|
|
676
|
+
// advises removing.
|
|
677
|
+
|
|
678
|
+
self.viewCount++;
|
|
679
|
+
|
|
680
|
+
@try {
|
|
681
|
+
BOOL isWebView = self.config.detectWebViews && [self isWebView:view];
|
|
682
|
+
BOOL isCamera = self.config.detectCameraViews && [self isCameraPreview:view];
|
|
683
|
+
BOOL isVideo = self.config.detectVideoLayers && [self isVideoLayerView:view];
|
|
684
|
+
BOOL isBlockedSurface = isWebView || isCamera || isVideo;
|
|
685
|
+
|
|
686
|
+
[self checkSensitiveView:view inWindow:window];
|
|
687
|
+
|
|
688
|
+
if ([view isKindOfClass:[UIScrollView class]]) {
|
|
689
|
+
[self trackScrollView:(UIScrollView *)view];
|
|
690
|
+
} else if (isWebView && [view respondsToSelector:@selector(scrollView)]) {
|
|
691
|
+
@try {
|
|
692
|
+
UIScrollView *webScrollView = [view valueForKey:@"scrollView"];
|
|
693
|
+
if ([webScrollView isKindOfClass:[UIScrollView class]]) {
|
|
694
|
+
[self trackScrollView:webScrollView];
|
|
695
|
+
}
|
|
696
|
+
} @catch (NSException *exception) {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (isBlockedSurface) {
|
|
701
|
+
[self appendBlockedSurfaceInfoToSignature:view depth:depth];
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
[self appendViewInfoToSignature:view depth:depth];
|
|
706
|
+
[self trackAnimationsForView:view inWindow:window];
|
|
707
|
+
|
|
708
|
+
NSArray *subviews = view.subviews;
|
|
709
|
+
if (subviews) {
|
|
710
|
+
for (UIView *subview in [subviews reverseObjectEnumerator]) {
|
|
711
|
+
@try {
|
|
712
|
+
[self scanView:subview inWindow:window depth:depth + 1];
|
|
713
|
+
} @catch (NSException *e) {
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} @catch (NSException *exception) {
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
#pragma mark - Layout Signature Generation
|
|
722
|
+
|
|
723
|
+
- (void)mixInt:(int32_t)val {
|
|
724
|
+
self.layoutSignatureHash =
|
|
725
|
+
fnv1a_u64(self.layoutSignatureHash, &val, sizeof(val));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
- (void)mixPtr:(const void *)ptr {
|
|
729
|
+
uintptr_t val = (uintptr_t)ptr;
|
|
730
|
+
self.layoutSignatureHash =
|
|
731
|
+
fnv1a_u64(self.layoutSignatureHash, &val, sizeof(val));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
- (void)appendViewInfoToSignature:(UIView *)view depth:(NSInteger)depth {
|
|
735
|
+
if (!view)
|
|
736
|
+
return;
|
|
737
|
+
|
|
738
|
+
@try {
|
|
739
|
+
// 1. Mix depth
|
|
740
|
+
[self mixInt:(int32_t)depth];
|
|
741
|
+
|
|
742
|
+
// 2. Mix class pointer (faster than string name)
|
|
743
|
+
[self mixPtr:(__bridge const void *)[view class]];
|
|
744
|
+
|
|
745
|
+
// 3. Mix frame
|
|
746
|
+
CGRect f = view.frame;
|
|
747
|
+
int32_t x = (int32_t)lrintf(isfinite(f.origin.x) ? f.origin.x : 0);
|
|
748
|
+
int32_t y = (int32_t)lrintf(isfinite(f.origin.y) ? f.origin.y : 0);
|
|
749
|
+
int32_t w = (int32_t)lrintf(isfinite(f.size.width) ? f.size.width : 0);
|
|
750
|
+
int32_t h = (int32_t)lrintf(isfinite(f.size.height) ? f.size.height : 0);
|
|
751
|
+
[self mixInt:x];
|
|
752
|
+
[self mixInt:y];
|
|
753
|
+
[self mixInt:w];
|
|
754
|
+
[self mixInt:h];
|
|
755
|
+
|
|
756
|
+
// 4. Mix scroll offset
|
|
757
|
+
if ([view isKindOfClass:[UIScrollView class]]) {
|
|
758
|
+
CGPoint o = ((UIScrollView *)view).contentOffset;
|
|
759
|
+
[self mixInt:(int32_t)lrintf(isfinite(o.x) ? o.x : 0)];
|
|
760
|
+
[self mixInt:(int32_t)lrintf(isfinite(o.y) ? o.y : 0)];
|
|
761
|
+
|
|
762
|
+
UIEdgeInsets inset = ((UIScrollView *)view).contentInset;
|
|
763
|
+
[self mixInt:(int32_t)lrintf(isfinite(inset.top) ? inset.top * 100 : 0)];
|
|
764
|
+
[self mixInt:(int32_t)lrintf(isfinite(inset.bottom) ? inset.bottom * 100
|
|
765
|
+
: 0)];
|
|
766
|
+
[self mixInt:(int32_t)lrintf(isfinite(inset.left) ? inset.left * 100 : 0)];
|
|
767
|
+
[self mixInt:(int32_t)lrintf(isfinite(inset.right) ? inset.right * 100
|
|
768
|
+
: 0)];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// 5. Mix Text Content (avoid input content; use length only)
|
|
772
|
+
if ([view isKindOfClass:[UITextField class]]) {
|
|
773
|
+
NSString *text = ((UITextField *)view).text;
|
|
774
|
+
[self mixInt:(int32_t)text.length];
|
|
775
|
+
} else if ([view isKindOfClass:[UITextView class]]) {
|
|
776
|
+
UITextView *textView = (UITextView *)view;
|
|
777
|
+
if (textView.isEditable) {
|
|
778
|
+
[self mixInt:(int32_t)textView.text.length];
|
|
779
|
+
} else if (textView.text.length > 0) {
|
|
780
|
+
NSUInteger th = textView.text.hash;
|
|
781
|
+
self.layoutSignatureHash =
|
|
782
|
+
fnv1a_u64(self.layoutSignatureHash, &th, sizeof(th));
|
|
783
|
+
}
|
|
784
|
+
} else if ([view isKindOfClass:[UILabel class]]) {
|
|
785
|
+
NSString *text = ((UILabel *)view).text;
|
|
786
|
+
if (text.length > 0) {
|
|
787
|
+
NSUInteger th = text.hash;
|
|
788
|
+
self.layoutSignatureHash =
|
|
789
|
+
fnv1a_u64(self.layoutSignatureHash, &th, sizeof(th));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// 6. Mix Accessibility Label (Critical for RN view recycling)
|
|
794
|
+
// React Native often recycles views or uses generic RCTViews for text
|
|
795
|
+
// containers but updates the accessibility label. This is a very cheap way
|
|
796
|
+
// to detect changes.
|
|
797
|
+
NSString *axLabel = view.accessibilityLabel;
|
|
798
|
+
if (axLabel.length > 0) {
|
|
799
|
+
NSUInteger axh = axLabel.hash;
|
|
800
|
+
self.layoutSignatureHash =
|
|
801
|
+
fnv1a_u64(self.layoutSignatureHash, &axh, sizeof(axh));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// 7. Mix Image pointer (was 6)
|
|
805
|
+
if ([view isKindOfClass:[UIImageView class]]) {
|
|
806
|
+
UIImage *img = ((UIImageView *)view).image;
|
|
807
|
+
if (img) {
|
|
808
|
+
[self mixPtr:(__bridge const void *)img];
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// 7b. Mix Map camera state when available
|
|
813
|
+
if ([self isMapView:view]) {
|
|
814
|
+
NSString *mapSignature = [self mapSignatureForView:view];
|
|
815
|
+
if (mapSignature.length > 0) {
|
|
816
|
+
NSUInteger mh = mapSignature.hash;
|
|
817
|
+
self.layoutSignatureHash =
|
|
818
|
+
fnv1a_u64(self.layoutSignatureHash, &mh, sizeof(mh));
|
|
819
|
+
}
|
|
820
|
+
[self mixInt:(int32_t)([self isMapViewLoading:view] ? 1 : 0)];
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 8. Mix Background Color
|
|
824
|
+
if (view.backgroundColor) {
|
|
825
|
+
[self mixInt:(int32_t)[view.backgroundColor hash]];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// 8b. Mix Tint Color (captures template image/color changes)
|
|
829
|
+
if (view.tintColor) {
|
|
830
|
+
[self mixInt:(int32_t)[view.tintColor hash]];
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// 9. Mix Alpha (to 1% precision)
|
|
834
|
+
[self mixInt:(int32_t)(view.alpha * 100)];
|
|
835
|
+
|
|
836
|
+
// 10. Mix Hidden State
|
|
837
|
+
[self mixInt:(int32_t)(view.hidden ? 1 : 0)];
|
|
838
|
+
|
|
839
|
+
} @catch (NSException *exception) {
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
- (void)appendBlockedSurfaceInfoToSignature:(UIView *)view depth:(NSInteger)depth {
|
|
844
|
+
if (!view) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
@try {
|
|
849
|
+
[self mixInt:(int32_t)depth];
|
|
850
|
+
[self mixPtr:(__bridge const void *)[view class]];
|
|
851
|
+
|
|
852
|
+
CGRect f = view.frame;
|
|
853
|
+
int32_t x = (int32_t)lrintf(isfinite(f.origin.x) ? f.origin.x : 0);
|
|
854
|
+
int32_t y = (int32_t)lrintf(isfinite(f.origin.y) ? f.origin.y : 0);
|
|
855
|
+
int32_t w = (int32_t)lrintf(isfinite(f.size.width) ? f.size.width : 0);
|
|
856
|
+
int32_t h = (int32_t)lrintf(isfinite(f.size.height) ? f.size.height : 0);
|
|
857
|
+
[self mixInt:x];
|
|
858
|
+
[self mixInt:y];
|
|
859
|
+
[self mixInt:w];
|
|
860
|
+
[self mixInt:h];
|
|
861
|
+
|
|
862
|
+
[self mixInt:(int32_t)(view.alpha * 100)];
|
|
863
|
+
[self mixInt:(int32_t)(view.hidden ? 1 : 0)];
|
|
864
|
+
} @catch (NSException *exception) {
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
#pragma mark - Sensitive View Detection
|
|
869
|
+
|
|
870
|
+
- (void)checkSensitiveView:(UIView *)view inWindow:(UIWindow *)window {
|
|
871
|
+
BOOL isTextInput =
|
|
872
|
+
self.config.detectTextInputs && [self isActualTextInput:view];
|
|
873
|
+
BOOL isCamera = self.config.detectCameraViews && [self isCameraPreview:view];
|
|
874
|
+
BOOL isWebView = self.config.detectWebViews && [self isWebView:view];
|
|
875
|
+
BOOL isVideo = self.config.detectVideoLayers && [self isVideoLayerView:view];
|
|
876
|
+
BOOL isManuallyMasked = [self isManuallyMaskedView:view];
|
|
877
|
+
|
|
878
|
+
// Check for MapView - capture frame and pointer for hybrid capture strategy
|
|
879
|
+
BOOL isMapView = [self isMapView:view];
|
|
880
|
+
|
|
881
|
+
// Determine the target window for coordinate conversion
|
|
882
|
+
// If primaryCaptureWindow is set (multi-window scan), use it
|
|
883
|
+
// Otherwise fall back to the passed window parameter
|
|
884
|
+
UIWindow *targetWindow = self.primaryCaptureWindow ?: window;
|
|
885
|
+
if (!targetWindow) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (isMapView) {
|
|
890
|
+
self.foundMapView = YES;
|
|
891
|
+
|
|
892
|
+
if ([self isMapViewLoading:view]) {
|
|
893
|
+
self.scanMapActive = YES;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if ([self isMapViewGestureActive:view]) {
|
|
897
|
+
self.scanMapActive = YES;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
NSString *mapSignature = [self mapSignatureForView:view];
|
|
901
|
+
if (mapSignature.length > 0) {
|
|
902
|
+
NSString *previous = [self.mapStateCache objectForKey:view];
|
|
903
|
+
if (previous && ![previous isEqualToString:mapSignature]) {
|
|
904
|
+
self.scanMapActive = YES;
|
|
905
|
+
}
|
|
906
|
+
[self.mapStateCache setObject:mapSignature forKey:view];
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Capture the MapView frame and pointer for hybrid capture
|
|
910
|
+
// Always convert through screen coordinates for consistency
|
|
911
|
+
CGRect frameInScreen = CGRectZero;
|
|
912
|
+
CGRect frameInTarget = CGRectZero;
|
|
913
|
+
@try {
|
|
914
|
+
frameInScreen = [view convertRect:view.bounds toView:nil];
|
|
915
|
+
frameInTarget = [targetWindow convertRect:frameInScreen fromWindow:nil];
|
|
916
|
+
} @catch (NSException *exception) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (!CGRectIsEmpty(frameInTarget) && frameInTarget.size.width > 10 &&
|
|
921
|
+
frameInTarget.size.height > 10 &&
|
|
922
|
+
CGRectIntersectsRect(frameInTarget, targetWindow.bounds)) {
|
|
923
|
+
[self.mutableMapViewFrames
|
|
924
|
+
addObject:[NSValue valueWithCGRect:frameInTarget]];
|
|
925
|
+
[self.mutableMapViewPointers
|
|
926
|
+
addObject:[NSValue valueWithNonretainedObject:view]];
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (isTextInput || isCamera || isWebView || isVideo || isManuallyMasked) {
|
|
931
|
+
// CRITICAL FIX: Always convert through screen coordinates
|
|
932
|
+
// This ensures correct coordinate conversion even when:
|
|
933
|
+
// 1. The view is in a different window (modal, overlay, React Native
|
|
934
|
+
// navigation)
|
|
935
|
+
// 2. Multi-window scanning is active (primaryCaptureWindow is set)
|
|
936
|
+
// 3. Windows have different transforms or scales
|
|
937
|
+
//
|
|
938
|
+
// Step 1: Convert view bounds to screen coordinates (toView:nil)
|
|
939
|
+
// Step 2: Convert screen coordinates to target window coordinates
|
|
940
|
+
// (fromWindow:nil)
|
|
941
|
+
CGRect frameInScreen = CGRectZero;
|
|
942
|
+
CGRect frameInTarget = CGRectZero;
|
|
943
|
+
@try {
|
|
944
|
+
frameInScreen = [view convertRect:view.bounds toView:nil];
|
|
945
|
+
frameInTarget = [targetWindow convertRect:frameInScreen fromWindow:nil];
|
|
946
|
+
} @catch (NSException *exception) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// If conversion failed (view not in window hierarchy), try direct
|
|
951
|
+
// conversion
|
|
952
|
+
if (CGRectIsEmpty(frameInTarget) ||
|
|
953
|
+
(frameInTarget.origin.x == 0 && frameInTarget.origin.y == 0 &&
|
|
954
|
+
frameInTarget.size.width == 0 && frameInTarget.size.height == 0)) {
|
|
955
|
+
// Fallback: try direct conversion if view is in target window
|
|
956
|
+
if (view.window == targetWindow) {
|
|
957
|
+
@try {
|
|
958
|
+
frameInTarget = [view convertRect:view.bounds toView:targetWindow];
|
|
959
|
+
} @catch (NSException *exception) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Sanitize NaN values before storing to prevent CoreGraphics errors
|
|
966
|
+
CGFloat x = isnan(frameInTarget.origin.x) ? 0 : frameInTarget.origin.x;
|
|
967
|
+
CGFloat y = isnan(frameInTarget.origin.y) ? 0 : frameInTarget.origin.y;
|
|
968
|
+
CGFloat w = isnan(frameInTarget.size.width) ? 0 : frameInTarget.size.width;
|
|
969
|
+
CGFloat h =
|
|
970
|
+
isnan(frameInTarget.size.height) ? 0 : frameInTarget.size.height;
|
|
971
|
+
|
|
972
|
+
// Also check for infinity values
|
|
973
|
+
if (isinf(x))
|
|
974
|
+
x = 0;
|
|
975
|
+
if (isinf(y))
|
|
976
|
+
y = 0;
|
|
977
|
+
if (isinf(w))
|
|
978
|
+
w = 0;
|
|
979
|
+
if (isinf(h))
|
|
980
|
+
h = 0;
|
|
981
|
+
|
|
982
|
+
CGRect sanitizedFrame = CGRectMake(x, y, w, h);
|
|
983
|
+
|
|
984
|
+
if (!CGRectIsEmpty(sanitizedFrame) && sanitizedFrame.size.width > 10 &&
|
|
985
|
+
sanitizedFrame.size.height > 10 &&
|
|
986
|
+
CGRectIntersectsRect(sanitizedFrame, targetWindow.bounds)) {
|
|
987
|
+
|
|
988
|
+
NSValue *frameValue = [NSValue valueWithCGRect:sanitizedFrame];
|
|
989
|
+
|
|
990
|
+
if (isCamera) {
|
|
991
|
+
[self.mutableCameraFrames addObject:frameValue];
|
|
992
|
+
} else if (isWebView) {
|
|
993
|
+
[self.mutableWebViewFrames addObject:frameValue];
|
|
994
|
+
} else if (isVideo) {
|
|
995
|
+
[self.mutableVideoFrames addObject:frameValue];
|
|
996
|
+
} else {
|
|
997
|
+
[self.mutableTextInputFrames addObject:frameValue];
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
#ifdef DEBUG
|
|
1001
|
+
RJLogDebug(@"ViewHierarchyScanner: Found %@ at (%.0f,%.0f,%.0f,%.0f) - "
|
|
1002
|
+
@"view.window=%@ targetWindow=%@",
|
|
1003
|
+
isTextInput
|
|
1004
|
+
? @"TextInput"
|
|
1005
|
+
: (isCamera
|
|
1006
|
+
? @"Camera"
|
|
1007
|
+
: (isWebView ? @"WebView"
|
|
1008
|
+
: (isVideo ? @"Video" : @"MaskedView"))),
|
|
1009
|
+
sanitizedFrame.origin.x, sanitizedFrame.origin.y,
|
|
1010
|
+
sanitizedFrame.size.width, sanitizedFrame.size.height,
|
|
1011
|
+
NSStringFromClass([view.window class]),
|
|
1012
|
+
NSStringFromClass([targetWindow class]));
|
|
1013
|
+
#endif
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
#pragma mark - Privacy Fallback Traversal
|
|
1019
|
+
|
|
1020
|
+
/// Targeted traversal that ONLY checks for sensitive views.
|
|
1021
|
+
/// Used when the main traversal hits view limits and finds no text inputs.
|
|
1022
|
+
- (void)scanSensitiveViewsOnlyInWindow:(UIWindow *)window {
|
|
1023
|
+
if (!window)
|
|
1024
|
+
return;
|
|
1025
|
+
|
|
1026
|
+
@try {
|
|
1027
|
+
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
|
|
1028
|
+
const CFTimeInterval maxTime = 0.010; // 10ms budget (aggressive)
|
|
1029
|
+
const NSUInteger maxViews = 2000; // Reduced from 4000
|
|
1030
|
+
|
|
1031
|
+
NSMutableArray<UIView *> *queue = [NSMutableArray arrayWithCapacity:256];
|
|
1032
|
+
[queue addObject:window];
|
|
1033
|
+
|
|
1034
|
+
NSUInteger scanned = 0;
|
|
1035
|
+
NSUInteger headIndex = 0; // Optimization: Use index pointer (O(1) pop)
|
|
1036
|
+
|
|
1037
|
+
while (headIndex < queue.count) {
|
|
1038
|
+
if (scanned >= maxViews) {
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
if ((CFAbsoluteTimeGetCurrent() - start) > maxTime) {
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
UIView *current = queue[headIndex++];
|
|
1046
|
+
if (!current)
|
|
1047
|
+
continue;
|
|
1048
|
+
|
|
1049
|
+
if ([self isViewVisible:current]) {
|
|
1050
|
+
[self checkSensitiveView:current inWindow:window];
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
NSArray<UIView *> *subviews = current.subviews;
|
|
1054
|
+
if (subviews.count > 0) {
|
|
1055
|
+
[queue addObjectsFromArray:subviews];
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
scanned++;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
#ifdef DEBUG
|
|
1062
|
+
if (self.mutableTextInputFrames.count > 0) {
|
|
1063
|
+
RJLogWarning(@"ViewHierarchyScanner: Privacy fallback found %lu text "
|
|
1064
|
+
@"inputs after main scan hit view limit",
|
|
1065
|
+
(unsigned long)self.mutableTextInputFrames.count);
|
|
1066
|
+
} else {
|
|
1067
|
+
RJLogWarning(@"ViewHierarchyScanner: Privacy fallback found 0 text "
|
|
1068
|
+
@"inputs (window=%@)",
|
|
1069
|
+
NSStringFromClass([window class]));
|
|
1070
|
+
}
|
|
1071
|
+
#endif
|
|
1072
|
+
} @catch (NSException *exception) {
|
|
1073
|
+
RJLogWarning(@"ViewHierarchyScanner: Privacy fallback failed: %@",
|
|
1074
|
+
exception);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
#pragma mark - Class Name Caching
|
|
1079
|
+
|
|
1080
|
+
- (NSString *)cachedClassNameForClass:(Class)cls {
|
|
1081
|
+
NSString *cached = [self.classNameCache objectForKey:cls];
|
|
1082
|
+
if (cached)
|
|
1083
|
+
return cached;
|
|
1084
|
+
|
|
1085
|
+
NSString *name = NSStringFromClass(cls);
|
|
1086
|
+
[self.classNameCache setObject:name forKey:cls];
|
|
1087
|
+
return name;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
#pragma mark - Sensitive View Detection Helpers
|
|
1091
|
+
|
|
1092
|
+
- (BOOL)isActualTextInput:(UIView *)view {
|
|
1093
|
+
if (!view)
|
|
1094
|
+
return NO;
|
|
1095
|
+
|
|
1096
|
+
// Start with direct class checks for common inputs
|
|
1097
|
+
if ([view isKindOfClass:[UITextField class]] ||
|
|
1098
|
+
[view isKindOfClass:[UITextView class]] ||
|
|
1099
|
+
[view isKindOfClass:[UISearchBar class]]) {
|
|
1100
|
+
if ([view isKindOfClass:[UITextView class]]) {
|
|
1101
|
+
return ((UITextView *)view).isEditable;
|
|
1102
|
+
}
|
|
1103
|
+
return YES;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// 2. Fast resolved-class check (RN inputs)
|
|
1107
|
+
for (Class cls in self.resolvedRnInputClasses) {
|
|
1108
|
+
if ([view isKindOfClass:cls]) {
|
|
1109
|
+
return YES;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// 3. Fallback: String heuristics (slow, only if needed)
|
|
1114
|
+
NSString *className = [self cachedClassNameForClass:[view class]];
|
|
1115
|
+
if ([className containsString:@"TextInput"] ||
|
|
1116
|
+
[className containsString:@"TextField"] ||
|
|
1117
|
+
[className containsString:@"TextEditor"]) {
|
|
1118
|
+
return YES;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// NOTE: B5 Optimization recommends removing the superclass crawl if not
|
|
1122
|
+
// strictly needed. We'll leave the string check as the final fallback but
|
|
1123
|
+
// rely on resolved classes for 99% of cases.
|
|
1124
|
+
|
|
1125
|
+
return NO;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
- (BOOL)isCameraPreview:(UIView *)view {
|
|
1129
|
+
if (!view)
|
|
1130
|
+
return NO;
|
|
1131
|
+
|
|
1132
|
+
// 1. Fast resolved-class check
|
|
1133
|
+
for (Class cls in self.resolvedCameraClasses) {
|
|
1134
|
+
if ([view isKindOfClass:cls]) {
|
|
1135
|
+
return YES;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// 2. Fast layer check
|
|
1140
|
+
if ([view.layer isKindOfClass:self.cachedAVCapturePreviewLayer]) {
|
|
1141
|
+
return YES;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
for (CALayer *sublayer in view.layer.sublayers) {
|
|
1145
|
+
if ([sublayer isKindOfClass:self.cachedAVCapturePreviewLayer]) {
|
|
1146
|
+
return YES;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// 3. Fallback: String heuristics
|
|
1151
|
+
NSString *className = [self cachedClassNameForClass:[view class]];
|
|
1152
|
+
return [self.cameraClasses containsObject:className] ||
|
|
1153
|
+
[className containsString:@"Camera"] ||
|
|
1154
|
+
[className containsString:@"Preview"] ||
|
|
1155
|
+
[className containsString:@"Scanner"];
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
- (BOOL)isWebView:(UIView *)view {
|
|
1159
|
+
if (!view)
|
|
1160
|
+
return NO;
|
|
1161
|
+
|
|
1162
|
+
Class wkClass = NSClassFromString(@"WKWebView");
|
|
1163
|
+
if (wkClass && [view isKindOfClass:wkClass]) {
|
|
1164
|
+
return YES;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
for (Class cls in self.resolvedWebViewClasses) {
|
|
1168
|
+
if ([view isKindOfClass:cls]) {
|
|
1169
|
+
return YES;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
NSString *className = [self cachedClassNameForClass:[view class]];
|
|
1174
|
+
return [self.webViewClasses containsObject:className] ||
|
|
1175
|
+
[className containsString:@"WebView"] ||
|
|
1176
|
+
[className containsString:@"WKContentView"];
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
- (BOOL)isVideoLayerView:(UIView *)view {
|
|
1180
|
+
if (!view)
|
|
1181
|
+
return NO;
|
|
1182
|
+
|
|
1183
|
+
if ([view.layer isKindOfClass:self.cachedAVPlayerLayer]) {
|
|
1184
|
+
return YES;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
for (CALayer *sublayer in view.layer.sublayers) {
|
|
1188
|
+
if ([sublayer isKindOfClass:self.cachedAVPlayerLayer]) {
|
|
1189
|
+
return YES;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
NSString *className = [self cachedClassNameForClass:[view class]];
|
|
1194
|
+
return ([className containsString:@"Video"] &&
|
|
1195
|
+
[className containsString:@"View"]);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
- (BOOL)isMapView:(UIView *)view {
|
|
1199
|
+
if (!view)
|
|
1200
|
+
return NO;
|
|
1201
|
+
|
|
1202
|
+
NSString *className = [self cachedClassNameForClass:[view class]];
|
|
1203
|
+
return [self.mapViewClasses containsObject:className] ||
|
|
1204
|
+
[className containsString:@"MapView"] ||
|
|
1205
|
+
[className containsString:@"MKMapView"];
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
- (BOOL)isMapViewLoading:(UIView *)view {
|
|
1209
|
+
if (!view) {
|
|
1210
|
+
return NO;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
@try {
|
|
1214
|
+
if ([view respondsToSelector:@selector(isLoading)]) {
|
|
1215
|
+
BOOL (*loadingMsg)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
|
|
1216
|
+
return loadingMsg(view, @selector(isLoading));
|
|
1217
|
+
}
|
|
1218
|
+
id loadingValue = [view valueForKey:@"loading"];
|
|
1219
|
+
if ([loadingValue respondsToSelector:@selector(boolValue)]) {
|
|
1220
|
+
return [loadingValue boolValue];
|
|
1221
|
+
}
|
|
1222
|
+
} @catch (NSException *exception) {
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return NO;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
- (BOOL)isMapViewGestureActive:(UIView *)view {
|
|
1229
|
+
if (!view) {
|
|
1230
|
+
return NO;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
for (UIGestureRecognizer *recognizer in view.gestureRecognizers) {
|
|
1234
|
+
if (recognizer.state == UIGestureRecognizerStateBegan ||
|
|
1235
|
+
recognizer.state == UIGestureRecognizerStateChanged ||
|
|
1236
|
+
recognizer.state == UIGestureRecognizerStateEnded) {
|
|
1237
|
+
return YES;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return NO;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
- (BOOL)hasPresentationDeltaForView:(UIView *)view {
|
|
1245
|
+
CALayer *presentation = view.layer.presentationLayer;
|
|
1246
|
+
if (!presentation) {
|
|
1247
|
+
return NO;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
CGRect modelFrame = view.layer.frame;
|
|
1251
|
+
CGRect presentationFrame = presentation.frame;
|
|
1252
|
+
|
|
1253
|
+
if (!isfinite(modelFrame.origin.x) || !isfinite(modelFrame.origin.y) ||
|
|
1254
|
+
!isfinite(modelFrame.size.width) || !isfinite(modelFrame.size.height) ||
|
|
1255
|
+
!isfinite(presentationFrame.origin.x) ||
|
|
1256
|
+
!isfinite(presentationFrame.origin.y) ||
|
|
1257
|
+
!isfinite(presentationFrame.size.width) ||
|
|
1258
|
+
!isfinite(presentationFrame.size.height)) {
|
|
1259
|
+
return NO;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
CGFloat dx = fabs(presentationFrame.origin.x - modelFrame.origin.x);
|
|
1263
|
+
CGFloat dy = fabs(presentationFrame.origin.y - modelFrame.origin.y);
|
|
1264
|
+
CGFloat dw = fabs(presentationFrame.size.width - modelFrame.size.width);
|
|
1265
|
+
CGFloat dh = fabs(presentationFrame.size.height - modelFrame.size.height);
|
|
1266
|
+
|
|
1267
|
+
return (dx > kRJAnimationPresentationEpsilon ||
|
|
1268
|
+
dy > kRJAnimationPresentationEpsilon ||
|
|
1269
|
+
dw > kRJAnimationPresentationEpsilon ||
|
|
1270
|
+
dh > kRJAnimationPresentationEpsilon);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
- (BOOL)isManuallyMaskedView:(UIView *)view {
|
|
1274
|
+
if ([view.accessibilityHint isEqualToString:@"rejourney_occlude"]) {
|
|
1275
|
+
return YES;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
NSString *nativeID = view.accessibilityIdentifier;
|
|
1279
|
+
if (nativeID.length > 0 &&
|
|
1280
|
+
[self.config.maskedNativeIDs containsObject:nativeID]) {
|
|
1281
|
+
return YES;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
NSArray *subviews = view.subviews;
|
|
1285
|
+
for (UIView *subview in [subviews reverseObjectEnumerator]) {
|
|
1286
|
+
NSString *childNativeID = subview.accessibilityIdentifier;
|
|
1287
|
+
if (childNativeID.length > 0 &&
|
|
1288
|
+
[self.config.maskedNativeIDs containsObject:childNativeID]) {
|
|
1289
|
+
return YES;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return NO;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
#pragma mark - Motion & Animation Tracking
|
|
1297
|
+
|
|
1298
|
+
- (void)trackScrollView:(UIScrollView *)scrollView {
|
|
1299
|
+
if (!scrollView) {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
[self.mutableScrollViewPointers
|
|
1304
|
+
addObject:[NSValue valueWithNonretainedObject:scrollView]];
|
|
1305
|
+
|
|
1306
|
+
RJScrollViewState *state = [self.scrollStateCache objectForKey:scrollView];
|
|
1307
|
+
if (!state) {
|
|
1308
|
+
state = [[RJScrollViewState alloc] init];
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
CGPoint offset = scrollView.contentOffset;
|
|
1312
|
+
UIEdgeInsets inset = scrollView.contentInset;
|
|
1313
|
+
CGFloat zoomScale = scrollView.zoomScale;
|
|
1314
|
+
|
|
1315
|
+
BOOL tracking = scrollView.isTracking || scrollView.isDragging ||
|
|
1316
|
+
scrollView.isDecelerating;
|
|
1317
|
+
BOOL offsetMoved = (fabs(offset.x - state.contentOffset.x) > kRJScrollEpsilon ||
|
|
1318
|
+
fabs(offset.y - state.contentOffset.y) > kRJScrollEpsilon);
|
|
1319
|
+
BOOL zoomMoved = fabs(zoomScale - state.zoomScale) > kRJZoomEpsilon;
|
|
1320
|
+
if (tracking || offsetMoved || zoomMoved) {
|
|
1321
|
+
self.scanScrollActive = YES;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
BOOL insetChanged =
|
|
1325
|
+
(fabs(inset.top - state.contentInset.top) > kRJInsetEpsilon ||
|
|
1326
|
+
fabs(inset.bottom - state.contentInset.bottom) > kRJInsetEpsilon ||
|
|
1327
|
+
fabs(inset.left - state.contentInset.left) > kRJInsetEpsilon ||
|
|
1328
|
+
fabs(inset.right - state.contentInset.right) > kRJInsetEpsilon);
|
|
1329
|
+
if ([self isOverscrolling:scrollView offset:offset] || insetChanged) {
|
|
1330
|
+
self.scanBounceActive = YES;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if ([self isRefreshActiveForScrollView:scrollView
|
|
1334
|
+
offset:offset
|
|
1335
|
+
inset:inset]) {
|
|
1336
|
+
self.scanRefreshActive = YES;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
state.contentOffset = offset;
|
|
1340
|
+
state.contentInset = inset;
|
|
1341
|
+
state.zoomScale = zoomScale;
|
|
1342
|
+
[self.scrollStateCache setObject:state forKey:scrollView];
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
- (BOOL)isOverscrolling:(UIScrollView *)scrollView offset:(CGPoint)offset {
|
|
1346
|
+
UIEdgeInsets inset = UIEdgeInsetsZero;
|
|
1347
|
+
@try {
|
|
1348
|
+
inset = scrollView.adjustedContentInset;
|
|
1349
|
+
} @catch (NSException *exception) {
|
|
1350
|
+
inset = scrollView.contentInset;
|
|
1351
|
+
}
|
|
1352
|
+
CGFloat topLimit = -inset.top - kRJScrollEpsilon;
|
|
1353
|
+
CGFloat bottomLimit = scrollView.contentSize.height -
|
|
1354
|
+
scrollView.bounds.size.height +
|
|
1355
|
+
inset.bottom + kRJScrollEpsilon;
|
|
1356
|
+
if (offset.y < topLimit || offset.y > bottomLimit) {
|
|
1357
|
+
return YES;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
CGFloat leftLimit = -inset.left - kRJScrollEpsilon;
|
|
1361
|
+
CGFloat rightLimit = scrollView.contentSize.width -
|
|
1362
|
+
scrollView.bounds.size.width +
|
|
1363
|
+
inset.right + kRJScrollEpsilon;
|
|
1364
|
+
return (offset.x < leftLimit || offset.x > rightLimit);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
- (BOOL)isRefreshActiveForScrollView:(UIScrollView *)scrollView
|
|
1368
|
+
offset:(CGPoint)offset
|
|
1369
|
+
inset:(UIEdgeInsets)inset {
|
|
1370
|
+
UIRefreshControl *refreshControl = scrollView.refreshControl;
|
|
1371
|
+
if (!refreshControl) {
|
|
1372
|
+
return NO;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (refreshControl.isRefreshing) {
|
|
1376
|
+
return YES;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
CGFloat triggerOffset = -scrollView.adjustedContentInset.top -
|
|
1380
|
+
kRJScrollEpsilon;
|
|
1381
|
+
if (offset.y < triggerOffset) {
|
|
1382
|
+
return YES;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (refreshControl.superview) {
|
|
1386
|
+
CGRect inScroll = [refreshControl.superview convertRect:refreshControl.frame
|
|
1387
|
+
toView:scrollView];
|
|
1388
|
+
if (CGRectIntersectsRect(inScroll, scrollView.bounds)) {
|
|
1389
|
+
return YES;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
return NO;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
- (void)trackAnimationsForView:(UIView *)view inWindow:(UIWindow *)window {
|
|
1397
|
+
if (!view) {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
BOOL hasAnimationKeys = (view.layer.animationKeys.count > 0);
|
|
1402
|
+
BOOL hasPresentationDelta = [self hasPresentationDeltaForView:view];
|
|
1403
|
+
if (!hasAnimationKeys && !hasPresentationDelta) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
self.scanHasAnimations = YES;
|
|
1408
|
+
[self.mutableAnimatedViewPointers
|
|
1409
|
+
addObject:[NSValue valueWithNonretainedObject:view]];
|
|
1410
|
+
|
|
1411
|
+
UIWindow *targetWindow = self.primaryCaptureWindow ?: window;
|
|
1412
|
+
if (!targetWindow) {
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
CGRect frameInScreen = CGRectZero;
|
|
1416
|
+
CGRect frameInTarget = CGRectZero;
|
|
1417
|
+
@try {
|
|
1418
|
+
frameInScreen = [view convertRect:view.bounds toView:nil];
|
|
1419
|
+
frameInTarget = [targetWindow convertRect:frameInScreen fromWindow:nil];
|
|
1420
|
+
} @catch (NSException *exception) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (CGRectIsEmpty(frameInTarget) ||
|
|
1425
|
+
!CGRectIntersectsRect(frameInTarget, targetWindow.bounds)) {
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
CGRect visibleFrame = CGRectIntersection(frameInTarget, targetWindow.bounds);
|
|
1430
|
+
self.scanAnimatedArea += visibleFrame.size.width * visibleFrame.size.height;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
- (NSString *)mapSignatureForView:(UIView *)view {
|
|
1434
|
+
@try {
|
|
1435
|
+
NSValue *centerValue = [view valueForKey:@"centerCoordinate"];
|
|
1436
|
+
NSValue *spanValue = [view valueForKeyPath:@"region.span"];
|
|
1437
|
+
|
|
1438
|
+
RJCoordinate center = {0, 0};
|
|
1439
|
+
RJCoordinateSpan span = {0, 0};
|
|
1440
|
+
if ([centerValue isKindOfClass:[NSValue class]]) {
|
|
1441
|
+
[centerValue getValue:¢er];
|
|
1442
|
+
}
|
|
1443
|
+
if ([spanValue isKindOfClass:[NSValue class]]) {
|
|
1444
|
+
[spanValue getValue:&span];
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
NSNumber *altitude = [view valueForKeyPath:@"camera.altitude"];
|
|
1448
|
+
NSNumber *heading = [view valueForKeyPath:@"camera.heading"];
|
|
1449
|
+
NSNumber *pitch = [view valueForKeyPath:@"camera.pitch"];
|
|
1450
|
+
|
|
1451
|
+
double altitudeValue = altitude ? altitude.doubleValue : 0;
|
|
1452
|
+
double headingValue = heading ? heading.doubleValue : 0;
|
|
1453
|
+
double pitchValue = pitch ? pitch.doubleValue : 0;
|
|
1454
|
+
|
|
1455
|
+
return [NSString stringWithFormat:@"%.5f:%.5f:%.5f:%.5f:%.1f:%.1f:%.1f",
|
|
1456
|
+
center.latitude, center.longitude,
|
|
1457
|
+
span.latitudeDelta, span.longitudeDelta,
|
|
1458
|
+
altitudeValue, headingValue, pitchValue];
|
|
1459
|
+
} @catch (NSException *exception) {
|
|
1460
|
+
return @"";
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
@end
|