@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,922 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJPrivacyMask.m
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Privacy masking implementation using Core Graphics drawing.
|
|
6
|
+
// Draws blur overlays directly into the captured image without
|
|
7
|
+
// adding any views to the window hierarchy.
|
|
8
|
+
//
|
|
9
|
+
// Licensed under the Apache License, Version 2.0 (the "License").
|
|
10
|
+
// Copyright (c) 2026 Rejourney
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
#import "RJPrivacyMask.h"
|
|
14
|
+
#import "../Capture/RJViewHierarchyScanner.h"
|
|
15
|
+
#import "../Core/RJLogger.h"
|
|
16
|
+
#import <AVFoundation/AVFoundation.h>
|
|
17
|
+
#import <UIKit/UIKit.h>
|
|
18
|
+
#import <WebKit/WebKit.h>
|
|
19
|
+
|
|
20
|
+
static inline BOOL RJIsValidMaskFrame(CGRect frame) {
|
|
21
|
+
return isfinite(frame.origin.x) && isfinite(frame.origin.y) &&
|
|
22
|
+
isfinite(frame.size.width) && isfinite(frame.size.height) &&
|
|
23
|
+
frame.size.width > 0 && frame.size.height > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#pragma mark - Implementation
|
|
27
|
+
|
|
28
|
+
@interface RJPrivacyMask ()
|
|
29
|
+
|
|
30
|
+
@property(nonatomic, assign, readwrite) BOOL isInBackground;
|
|
31
|
+
|
|
32
|
+
@property(nonatomic, strong) NSMutableSet<NSValue *> *scannedViews;
|
|
33
|
+
|
|
34
|
+
@property(nonatomic, assign) BOOL lastFrameHadCamera;
|
|
35
|
+
|
|
36
|
+
@property(nonatomic, assign) BOOL lastFrameHadTextInput;
|
|
37
|
+
@property(nonatomic, assign) BOOL lastFrameHadWebView;
|
|
38
|
+
|
|
39
|
+
@property(nonatomic, assign) BOOL lastFrameHadVideo;
|
|
40
|
+
|
|
41
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *textInputFrames;
|
|
42
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *cameraFrames;
|
|
43
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *webViewFrames;
|
|
44
|
+
@property(nonatomic, strong) NSMutableArray<NSValue *> *videoFrames;
|
|
45
|
+
|
|
46
|
+
@property(nonatomic, strong, readwrite)
|
|
47
|
+
NSMutableSet<NSString *> *maskedNativeIDs;
|
|
48
|
+
|
|
49
|
+
@property(nonatomic, assign) CGColorSpaceRef commonColorSpace;
|
|
50
|
+
|
|
51
|
+
// Forward declarations to fix 'method not found' warnings/erros
|
|
52
|
+
- (void)findSensitiveViewsInWindow:(UIWindow *)window;
|
|
53
|
+
- (void)scanView:(UIView *)view inWindow:(UIWindow *)window;
|
|
54
|
+
- (void)drawBackgroundOverlayInContext:(CGContextRef)context
|
|
55
|
+
bounds:(CGRect)bounds
|
|
56
|
+
scale:(CGFloat)scale;
|
|
57
|
+
- (void)drawBlurRectInContext:(CGContextRef)context
|
|
58
|
+
frame:(CGRect)frame
|
|
59
|
+
maskType:(NSInteger)maskType;
|
|
60
|
+
- (void)drawMasksWithScanResult:(RJViewHierarchyScanResult *)scanResult
|
|
61
|
+
context:(CGContextRef)context
|
|
62
|
+
bounds:(CGRect)bounds
|
|
63
|
+
scale:(CGFloat)scale;
|
|
64
|
+
- (BOOL)shouldMaskAllForScanResult:(RJViewHierarchyScanResult *)scanResult;
|
|
65
|
+
|
|
66
|
+
@end
|
|
67
|
+
|
|
68
|
+
@implementation RJPrivacyMask
|
|
69
|
+
|
|
70
|
+
#pragma mark - Initialization
|
|
71
|
+
|
|
72
|
+
- (instancetype)init {
|
|
73
|
+
self = [super init];
|
|
74
|
+
if (self) {
|
|
75
|
+
_maskCameraViews = YES;
|
|
76
|
+
_maskWebViews = YES;
|
|
77
|
+
_maskVideoLayers = YES;
|
|
78
|
+
_blurCornerRadius = 8.0;
|
|
79
|
+
_maskPadding = 4.0;
|
|
80
|
+
_isInBackground = NO;
|
|
81
|
+
_scannedViews = [NSMutableSet new];
|
|
82
|
+
_textInputFrames = [NSMutableArray new];
|
|
83
|
+
_cameraFrames = [NSMutableArray new];
|
|
84
|
+
_webViewFrames = [NSMutableArray new];
|
|
85
|
+
_videoFrames = [NSMutableArray new];
|
|
86
|
+
_lastFrameHadCamera = NO;
|
|
87
|
+
_lastFrameHadTextInput = NO;
|
|
88
|
+
_lastFrameHadWebView = NO;
|
|
89
|
+
_lastFrameHadVideo = NO;
|
|
90
|
+
_maskedNativeIDs = [NSMutableSet new];
|
|
91
|
+
|
|
92
|
+
// Optimization #10: Cache color space once
|
|
93
|
+
_commonColorSpace = CGColorSpaceCreateDeviceRGB();
|
|
94
|
+
|
|
95
|
+
[[NSNotificationCenter defaultCenter]
|
|
96
|
+
addObserver:self
|
|
97
|
+
selector:@selector(appDidEnterBackground:)
|
|
98
|
+
name:UIApplicationDidEnterBackgroundNotification
|
|
99
|
+
object:nil];
|
|
100
|
+
[[NSNotificationCenter defaultCenter]
|
|
101
|
+
addObserver:self
|
|
102
|
+
selector:@selector(appWillEnterForeground:)
|
|
103
|
+
name:UIApplicationWillEnterForegroundNotification
|
|
104
|
+
object:nil];
|
|
105
|
+
[[NSNotificationCenter defaultCenter]
|
|
106
|
+
addObserver:self
|
|
107
|
+
selector:@selector(appDidBecomeActive:)
|
|
108
|
+
name:UIApplicationDidBecomeActiveNotification
|
|
109
|
+
object:nil];
|
|
110
|
+
}
|
|
111
|
+
return self;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#pragma mark - Notification Handlers
|
|
115
|
+
|
|
116
|
+
- (void)appDidEnterBackground:(NSNotification *)notification {
|
|
117
|
+
self.isInBackground = YES;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
- (void)appWillEnterForeground:(NSNotification *)notification {
|
|
121
|
+
// Optional: keep masked until active
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
- (void)appDidBecomeActive:(NSNotification *)notification {
|
|
125
|
+
self.isInBackground = NO;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
- (void)dealloc {
|
|
129
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
130
|
+
if (_commonColorSpace) {
|
|
131
|
+
CGColorSpaceRelease(_commonColorSpace);
|
|
132
|
+
_commonColorSpace = NULL;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
- (BOOL)shouldMaskAllForScanResult:(RJViewHierarchyScanResult *)scanResult {
|
|
137
|
+
if (!scanResult) {
|
|
138
|
+
return YES;
|
|
139
|
+
}
|
|
140
|
+
if (scanResult.didBailOutEarly && scanResult.totalViewsScanned == 0) {
|
|
141
|
+
return YES;
|
|
142
|
+
}
|
|
143
|
+
return NO;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
- (void)drawMasksForWindow:(UIWindow *)window
|
|
147
|
+
bounds:(CGRect)bounds
|
|
148
|
+
scale:(CGFloat)scale {
|
|
149
|
+
if (!window) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
154
|
+
if (!context) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
CGFloat safeScale =
|
|
159
|
+
(isfinite(scale) && scale > 0.0) ? scale : 1.0;
|
|
160
|
+
|
|
161
|
+
[self findSensitiveViewsInWindow:window];
|
|
162
|
+
|
|
163
|
+
RJViewHierarchyScanResult *scanResult =
|
|
164
|
+
[[RJViewHierarchyScanResult alloc] init];
|
|
165
|
+
scanResult.textInputFrames = [self.textInputFrames copy];
|
|
166
|
+
scanResult.cameraFrames = [self.cameraFrames copy];
|
|
167
|
+
scanResult.webViewFrames = [self.webViewFrames copy];
|
|
168
|
+
scanResult.videoFrames = [self.videoFrames copy];
|
|
169
|
+
|
|
170
|
+
if ([self shouldMaskAllForScanResult:scanResult] || self.isInBackground) {
|
|
171
|
+
[self drawBackgroundOverlayInContext:context bounds:bounds scale:safeScale];
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
[self drawMasksWithScanResult:scanResult
|
|
176
|
+
context:context
|
|
177
|
+
bounds:bounds
|
|
178
|
+
scale:safeScale];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
- (void)drawMasksWithScanResult:(RJViewHierarchyScanResult *)scanResult
|
|
182
|
+
bounds:(CGRect)bounds
|
|
183
|
+
scale:(CGFloat)scale {
|
|
184
|
+
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
185
|
+
if (!context) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
CGFloat safeScale =
|
|
190
|
+
(isfinite(scale) && scale > 0.0) ? scale : 1.0;
|
|
191
|
+
|
|
192
|
+
if ([self shouldMaskAllForScanResult:scanResult] || self.isInBackground) {
|
|
193
|
+
[self drawBackgroundOverlayInContext:context bounds:bounds scale:safeScale];
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
[self drawMasksWithScanResult:scanResult
|
|
198
|
+
context:context
|
|
199
|
+
bounds:bounds
|
|
200
|
+
scale:safeScale];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
- (UIImage *)applyMasksToImage:(UIImage *)image
|
|
204
|
+
scanResult:(RJViewHierarchyScanResult *)scanResult
|
|
205
|
+
isInBackground:(BOOL)isInBackground {
|
|
206
|
+
if (!image) {
|
|
207
|
+
return image;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
BOOL shouldMaskAll = isInBackground ||
|
|
211
|
+
[self shouldMaskAllForScanResult:scanResult];
|
|
212
|
+
BOOL hasFrames = (scanResult.textInputFrames.count > 0 ||
|
|
213
|
+
scanResult.cameraFrames.count > 0 ||
|
|
214
|
+
scanResult.webViewFrames.count > 0 ||
|
|
215
|
+
scanResult.videoFrames.count > 0);
|
|
216
|
+
if (!shouldMaskAll && !hasFrames) {
|
|
217
|
+
return image;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
CGSize size = image.size;
|
|
221
|
+
CGFloat imageScale = image.scale;
|
|
222
|
+
CGRect bounds = CGRectMake(0, 0, size.width, size.height);
|
|
223
|
+
|
|
224
|
+
UIGraphicsBeginImageContextWithOptions(size, YES, imageScale);
|
|
225
|
+
[image drawAtPoint:CGPointZero];
|
|
226
|
+
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
227
|
+
|
|
228
|
+
BOOL previousBackground = self.isInBackground;
|
|
229
|
+
self.isInBackground = isInBackground;
|
|
230
|
+
|
|
231
|
+
if (context) {
|
|
232
|
+
if (shouldMaskAll) {
|
|
233
|
+
[self drawBackgroundOverlayInContext:context bounds:bounds scale:1.0];
|
|
234
|
+
} else {
|
|
235
|
+
[self drawMasksWithScanResult:scanResult
|
|
236
|
+
context:context
|
|
237
|
+
bounds:bounds
|
|
238
|
+
scale:1.0];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
UIImage *maskedImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
243
|
+
UIGraphicsEndImageContext();
|
|
244
|
+
|
|
245
|
+
self.isInBackground = previousBackground;
|
|
246
|
+
return maskedImage ?: image;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
- (void)applyToPixelBuffer:(CVPixelBufferRef)pixelBuffer
|
|
250
|
+
withScanResult:(RJViewHierarchyScanResult *)scanResult
|
|
251
|
+
scale:(CGFloat)scale {
|
|
252
|
+
if (!pixelBuffer)
|
|
253
|
+
return;
|
|
254
|
+
|
|
255
|
+
CGFloat safeScale =
|
|
256
|
+
(isfinite(scale) && scale > 0.0) ? scale : 1.0;
|
|
257
|
+
BOOL shouldMaskAll = [self shouldMaskAllForScanResult:scanResult];
|
|
258
|
+
|
|
259
|
+
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
|
260
|
+
|
|
261
|
+
void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
262
|
+
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
|
263
|
+
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
|
264
|
+
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
|
265
|
+
|
|
266
|
+
// Optimization #10: Use cached color space
|
|
267
|
+
CGColorSpaceRef colorSpace = self.commonColorSpace;
|
|
268
|
+
if (!colorSpace) {
|
|
269
|
+
// Fallback just in case
|
|
270
|
+
colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
CGContextRef context = CGBitmapContextCreate(
|
|
274
|
+
baseAddress, width, height, 8, bytesPerRow, colorSpace,
|
|
275
|
+
kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
|
|
276
|
+
|
|
277
|
+
// If we created a fallback, release it. If cached, don't.
|
|
278
|
+
if (colorSpace != self.commonColorSpace) {
|
|
279
|
+
CGColorSpaceRelease(colorSpace);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (context) {
|
|
283
|
+
// PUSH context so high-level UIKit drawing (NSString drawAtPoint) works
|
|
284
|
+
UIGraphicsPushContext(context);
|
|
285
|
+
|
|
286
|
+
// CRITICAL FIX: Flip coordinate system to match UIKit (Top-Left origin)
|
|
287
|
+
// CoreGraphics defaults to Bottom-Left origin, so without this flip,
|
|
288
|
+
// masking rects are drawn vertically mirrored (wrong place).
|
|
289
|
+
CGContextTranslateCTM(context, 0, height);
|
|
290
|
+
CGContextScaleCTM(context, 1.0, -1.0);
|
|
291
|
+
|
|
292
|
+
// Apply correct scale transform (Points -> Pixels)
|
|
293
|
+
CGContextScaleCTM(context, safeScale, safeScale);
|
|
294
|
+
|
|
295
|
+
// If in background or privacy scan is unavailable, draw full overlay
|
|
296
|
+
if (self.isInBackground || shouldMaskAll) {
|
|
297
|
+
// Create bounds in points (since we scaled the CTM)
|
|
298
|
+
CGRect boundsPoints =
|
|
299
|
+
CGRectMake(0, 0, width / safeScale, height / safeScale);
|
|
300
|
+
[self drawBackgroundOverlayInContext:context
|
|
301
|
+
bounds:boundsPoints
|
|
302
|
+
scale:1.0];
|
|
303
|
+
} else {
|
|
304
|
+
// Draw standard masks
|
|
305
|
+
[self drawMasksWithScanResult:scanResult
|
|
306
|
+
context:context
|
|
307
|
+
bounds:CGRectMake(0, 0, width / safeScale,
|
|
308
|
+
height / safeScale)
|
|
309
|
+
scale:1.0];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
UIGraphicsPopContext();
|
|
313
|
+
CGContextRelease(context);
|
|
314
|
+
} else {
|
|
315
|
+
// Log sparingly or once to avoid spam
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
- (void)drawMasksWithScanResult:(RJViewHierarchyScanResult *)scanResult
|
|
322
|
+
context:(CGContextRef)context
|
|
323
|
+
bounds:(CGRect)bounds
|
|
324
|
+
scale:(CGFloat)scale {
|
|
325
|
+
if (!scanResult || !context) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@try {
|
|
330
|
+
self.lastFrameHadCamera = NO;
|
|
331
|
+
self.lastFrameHadTextInput = NO;
|
|
332
|
+
self.lastFrameHadWebView = NO;
|
|
333
|
+
self.lastFrameHadVideo = NO;
|
|
334
|
+
|
|
335
|
+
// Handle background state
|
|
336
|
+
if (self.isInBackground) {
|
|
337
|
+
// Only draw background overlay if we have a method for it
|
|
338
|
+
if ([self respondsToSelector:@selector
|
|
339
|
+
(drawBackgroundOverlayInContext:bounds:scale:)]) {
|
|
340
|
+
[self drawBackgroundOverlayInContext:context bounds:bounds scale:scale];
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Early exit if nothing to mask - saves ~1-2ms per frame
|
|
346
|
+
if (scanResult.textInputFrames.count == 0 &&
|
|
347
|
+
scanResult.cameraFrames.count == 0 &&
|
|
348
|
+
scanResult.webViewFrames.count == 0 &&
|
|
349
|
+
scanResult.videoFrames.count == 0) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (NSValue *frameValue in scanResult.textInputFrames) {
|
|
354
|
+
CGRect frame = [frameValue CGRectValue];
|
|
355
|
+
|
|
356
|
+
if (!RJIsValidMaskFrame(frame)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
frame = CGRectInset(frame, -self.maskPadding, -self.maskPadding);
|
|
361
|
+
if (!RJIsValidMaskFrame(frame)) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
[self drawBlurRectInContext:context
|
|
365
|
+
frame:frame
|
|
366
|
+
maskType:0]; // 0 = TextInput
|
|
367
|
+
self.lastFrameHadTextInput = YES;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (NSValue *frameValue in scanResult.cameraFrames) {
|
|
371
|
+
CGRect frame = [frameValue CGRectValue];
|
|
372
|
+
|
|
373
|
+
if (!RJIsValidMaskFrame(frame)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
frame = CGRectInset(frame, -self.maskPadding, -self.maskPadding);
|
|
378
|
+
if (!RJIsValidMaskFrame(frame)) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
[self drawBlurRectInContext:context frame:frame maskType:1]; // 1 = Camera
|
|
382
|
+
self.lastFrameHadCamera = YES;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (self.maskWebViews && scanResult.webViewFrames.count > 0) {
|
|
386
|
+
for (NSValue *frameValue in scanResult.webViewFrames) {
|
|
387
|
+
CGRect frame = [frameValue CGRectValue];
|
|
388
|
+
|
|
389
|
+
if (!RJIsValidMaskFrame(frame)) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
frame = CGRectInset(frame, -self.maskPadding, -self.maskPadding);
|
|
394
|
+
if (!RJIsValidMaskFrame(frame)) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
[self drawBlurRectInContext:context
|
|
398
|
+
frame:frame
|
|
399
|
+
maskType:2]; // 2 = WebView
|
|
400
|
+
self.lastFrameHadWebView = YES;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (self.maskVideoLayers && scanResult.videoFrames.count > 0) {
|
|
405
|
+
for (NSValue *frameValue in scanResult.videoFrames) {
|
|
406
|
+
CGRect frame = [frameValue CGRectValue];
|
|
407
|
+
|
|
408
|
+
if (!RJIsValidMaskFrame(frame)) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
frame = CGRectInset(frame, -self.maskPadding, -self.maskPadding);
|
|
413
|
+
if (!RJIsValidMaskFrame(frame)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
[self drawBlurRectInContext:context
|
|
417
|
+
frame:frame
|
|
418
|
+
maskType:3]; // 3 = Video
|
|
419
|
+
self.lastFrameHadVideo = YES;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} @catch (NSException *exception) {
|
|
423
|
+
RJLogWarning(@"Privacy mask drawing failed: %@", exception);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
- (void)drawBackgroundOverlayInContext:(CGContextRef)context
|
|
428
|
+
bounds:(CGRect)bounds
|
|
429
|
+
scale:(CGFloat)scale {
|
|
430
|
+
// Simple black overlay for backgrounding
|
|
431
|
+
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
|
|
432
|
+
CGContextFillRect(context, bounds);
|
|
433
|
+
|
|
434
|
+
// Optional: Draw text or logo? Keeping it simple/fast.
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
- (void)drawBlurRectInContext:(CGContextRef)context
|
|
438
|
+
frame:(CGRect)frame
|
|
439
|
+
maskType:(NSInteger)maskType {
|
|
440
|
+
// Validate frame to prevent CoreGraphics NaN errors
|
|
441
|
+
if (isnan(frame.origin.x) || isnan(frame.origin.y) ||
|
|
442
|
+
isnan(frame.size.width) || isnan(frame.size.height) ||
|
|
443
|
+
isinf(frame.origin.x) || isinf(frame.origin.y) ||
|
|
444
|
+
isinf(frame.size.width) || isinf(frame.size.height)) {
|
|
445
|
+
RJLogWarning(@"PrivacyMask: Skipping invalid frame with NaN/Inf values");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Also skip zero or negative sized frames
|
|
450
|
+
if (frame.size.width <= 0 || frame.size.height <= 0) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
CGContextSaveGState(context);
|
|
455
|
+
|
|
456
|
+
UIBezierPath *path =
|
|
457
|
+
[UIBezierPath bezierPathWithRoundedRect:frame
|
|
458
|
+
cornerRadius:self.blurCornerRadius];
|
|
459
|
+
|
|
460
|
+
UIColor *blurColor;
|
|
461
|
+
if (maskType == 1 || maskType == 3) { // Camera / Video
|
|
462
|
+
blurColor = [UIColor colorWithRed:0.12 green:0.12 blue:0.15 alpha:1.0];
|
|
463
|
+
} else if (maskType == 2) { // WebView - Use same style as Camera as requested
|
|
464
|
+
blurColor = [UIColor colorWithRed:0.12 green:0.12 blue:0.15 alpha:1.0];
|
|
465
|
+
} else {
|
|
466
|
+
// TextInput and others
|
|
467
|
+
blurColor = [UIColor blackColor];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
CGContextSetFillColorWithColor(context, blurColor.CGColor);
|
|
471
|
+
CGContextAddPath(context, path.CGPath);
|
|
472
|
+
CGContextFillPath(context);
|
|
473
|
+
|
|
474
|
+
CGContextSetStrokeColorWithColor(
|
|
475
|
+
context, [UIColor colorWithWhite:0.5 alpha:0.3].CGColor);
|
|
476
|
+
CGContextSetLineWidth(context, 0.5);
|
|
477
|
+
CGContextAddPath(context, path.CGPath);
|
|
478
|
+
CGContextStrokePath(context);
|
|
479
|
+
|
|
480
|
+
if (maskType == 1) {
|
|
481
|
+
[self drawCameraLabelInContext:context frame:frame];
|
|
482
|
+
} else if (maskType == 2) {
|
|
483
|
+
// Reuse camera label style but with different text
|
|
484
|
+
[self drawWebViewLabelInContext:context frame:frame];
|
|
485
|
+
} else if (maskType == 3) {
|
|
486
|
+
[self drawVideoLabelInContext:context frame:frame];
|
|
487
|
+
} else {
|
|
488
|
+
[self drawTextInputLabelInContext:context frame:frame];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
CGContextRestoreGState(context);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
- (void)drawCameraLabelInContext:(CGContextRef)context frame:(CGRect)frame {
|
|
495
|
+
// Skip if frame has invalid values
|
|
496
|
+
if (isnan(frame.size.width) || isnan(frame.size.height) ||
|
|
497
|
+
frame.size.width <= 0 || frame.size.height <= 0) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
CGFloat centerX = CGRectGetMidX(frame);
|
|
502
|
+
CGFloat centerY = CGRectGetMidY(frame);
|
|
503
|
+
|
|
504
|
+
// Validate centers aren't NaN
|
|
505
|
+
if (isnan(centerX) || isnan(centerY)) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
NSString *label = @"📷 Camera Hidden";
|
|
510
|
+
UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
|
511
|
+
NSDictionary *attrs = @{
|
|
512
|
+
NSFontAttributeName : font,
|
|
513
|
+
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.7 alpha:1.0]
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
CGSize textSize = [label sizeWithAttributes:attrs];
|
|
517
|
+
|
|
518
|
+
if (textSize.width < frame.size.width - 20 &&
|
|
519
|
+
textSize.height < frame.size.height - 10) {
|
|
520
|
+
CGPoint textPoint = CGPointMake(centerX - textSize.width / 2,
|
|
521
|
+
centerY - textSize.height / 2);
|
|
522
|
+
[label drawAtPoint:textPoint withAttributes:attrs];
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
- (void)drawWebViewLabelInContext:(CGContextRef)context frame:(CGRect)frame {
|
|
527
|
+
// Skip if frame has invalid values
|
|
528
|
+
if (isnan(frame.size.width) || isnan(frame.size.height) ||
|
|
529
|
+
frame.size.width <= 0 || frame.size.height <= 0) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
CGFloat centerX = CGRectGetMidX(frame);
|
|
534
|
+
CGFloat centerY = CGRectGetMidY(frame);
|
|
535
|
+
|
|
536
|
+
// Validate centers aren't NaN
|
|
537
|
+
if (isnan(centerX) || isnan(centerY)) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
NSString *label = @"🌐 Web View Hidden";
|
|
542
|
+
UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
|
543
|
+
NSDictionary *attrs = @{
|
|
544
|
+
NSFontAttributeName : font,
|
|
545
|
+
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.7 alpha:1.0]
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
CGSize textSize = [label sizeWithAttributes:attrs];
|
|
549
|
+
|
|
550
|
+
if (textSize.width < frame.size.width - 20 &&
|
|
551
|
+
textSize.height < frame.size.height - 10) {
|
|
552
|
+
CGPoint textPoint = CGPointMake(centerX - textSize.width / 2,
|
|
553
|
+
centerY - textSize.height / 2);
|
|
554
|
+
[label drawAtPoint:textPoint withAttributes:attrs];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
- (void)drawVideoLabelInContext:(CGContextRef)context frame:(CGRect)frame {
|
|
559
|
+
if (isnan(frame.size.width) || isnan(frame.size.height) ||
|
|
560
|
+
frame.size.width <= 0 || frame.size.height <= 0) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
CGFloat centerX = CGRectGetMidX(frame);
|
|
565
|
+
CGFloat centerY = CGRectGetMidY(frame);
|
|
566
|
+
|
|
567
|
+
if (isnan(centerX) || isnan(centerY)) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
NSString *label = @"🎥 Video Hidden";
|
|
572
|
+
UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
|
573
|
+
NSDictionary *attrs = @{
|
|
574
|
+
NSFontAttributeName : font,
|
|
575
|
+
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.7 alpha:1.0]
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
CGSize textSize = [label sizeWithAttributes:attrs];
|
|
579
|
+
|
|
580
|
+
if (textSize.width < frame.size.width - 20 &&
|
|
581
|
+
textSize.height < frame.size.height - 10) {
|
|
582
|
+
CGPoint textPoint = CGPointMake(centerX - textSize.width / 2,
|
|
583
|
+
centerY - textSize.height / 2);
|
|
584
|
+
[label drawAtPoint:textPoint withAttributes:attrs];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
- (void)drawTextInputLabelInContext:(CGContextRef)context frame:(CGRect)frame {
|
|
589
|
+
// Skip if frame has invalid values
|
|
590
|
+
if (isnan(frame.size.width) || isnan(frame.size.height) ||
|
|
591
|
+
frame.size.width <= 0 || frame.size.height <= 0) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
CGFloat centerX = CGRectGetMidX(frame);
|
|
596
|
+
CGFloat centerY = CGRectGetMidY(frame);
|
|
597
|
+
|
|
598
|
+
// Validate centers aren't NaN
|
|
599
|
+
if (isnan(centerX) || isnan(centerY)) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
NSString *label = @"********";
|
|
604
|
+
UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
|
605
|
+
NSDictionary *attrs = @{
|
|
606
|
+
NSFontAttributeName : font,
|
|
607
|
+
NSForegroundColorAttributeName : [UIColor whiteColor]
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
CGSize textSize = [label sizeWithAttributes:attrs];
|
|
611
|
+
|
|
612
|
+
if (textSize.width < frame.size.width - 10 &&
|
|
613
|
+
textSize.height < frame.size.height - 4) {
|
|
614
|
+
CGPoint textPoint = CGPointMake(centerX - textSize.width / 2,
|
|
615
|
+
centerY - textSize.height / 2);
|
|
616
|
+
[label drawAtPoint:textPoint withAttributes:attrs];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#pragma mark - Sensitive View Detection
|
|
621
|
+
|
|
622
|
+
- (void)findSensitiveViewsInWindow:(UIWindow *)window {
|
|
623
|
+
if (!window) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
[self.scannedViews removeAllObjects];
|
|
628
|
+
[self.textInputFrames removeAllObjects];
|
|
629
|
+
[self.cameraFrames removeAllObjects];
|
|
630
|
+
[self.webViewFrames removeAllObjects];
|
|
631
|
+
[self.videoFrames removeAllObjects];
|
|
632
|
+
|
|
633
|
+
[self scanView:window inWindow:window];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
- (NSArray<NSValue *> *)findSensitiveFramesInWindow:(UIWindow *)window {
|
|
637
|
+
|
|
638
|
+
[self findSensitiveViewsInWindow:window];
|
|
639
|
+
|
|
640
|
+
NSMutableArray<NSValue *> *allFrames = [NSMutableArray new];
|
|
641
|
+
[allFrames addObjectsFromArray:self.textInputFrames];
|
|
642
|
+
[allFrames addObjectsFromArray:self.cameraFrames];
|
|
643
|
+
[allFrames addObjectsFromArray:self.webViewFrames];
|
|
644
|
+
[allFrames addObjectsFromArray:self.videoFrames];
|
|
645
|
+
|
|
646
|
+
return allFrames;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
- (void)scanView:(UIView *)view inWindow:(UIWindow *)window {
|
|
650
|
+
if (!view || view.isHidden || view.alpha < 0.01)
|
|
651
|
+
return;
|
|
652
|
+
|
|
653
|
+
NSValue *viewPtr = [NSValue valueWithNonretainedObject:view];
|
|
654
|
+
if ([self.scannedViews containsObject:viewPtr])
|
|
655
|
+
return;
|
|
656
|
+
[self.scannedViews addObject:viewPtr];
|
|
657
|
+
|
|
658
|
+
BOOL isTextInput = self.maskTextInputs && [self isActualTextInput:view];
|
|
659
|
+
BOOL isCamera = self.maskCameraViews && [self isCameraPreview:view];
|
|
660
|
+
BOOL isWebView = self.maskWebViews && [self isWebViewSurface:view];
|
|
661
|
+
BOOL isVideo = self.maskVideoLayers && [self isVideoLayerView:view];
|
|
662
|
+
BOOL isManuallyMasked = [self isManuallyMaskedView:view];
|
|
663
|
+
|
|
664
|
+
if (isTextInput || isCamera || isWebView || isVideo || isManuallyMasked) {
|
|
665
|
+
CGRect frameInWindow = [view convertRect:view.bounds toView:window];
|
|
666
|
+
|
|
667
|
+
// Sanitize NaN/Inf values from convertRect:toView:
|
|
668
|
+
CGFloat x = (isnan(frameInWindow.origin.x) || isinf(frameInWindow.origin.x))
|
|
669
|
+
? 0
|
|
670
|
+
: frameInWindow.origin.x;
|
|
671
|
+
CGFloat y = (isnan(frameInWindow.origin.y) || isinf(frameInWindow.origin.y))
|
|
672
|
+
? 0
|
|
673
|
+
: frameInWindow.origin.y;
|
|
674
|
+
CGFloat w =
|
|
675
|
+
(isnan(frameInWindow.size.width) || isinf(frameInWindow.size.width))
|
|
676
|
+
? 0
|
|
677
|
+
: frameInWindow.size.width;
|
|
678
|
+
CGFloat h =
|
|
679
|
+
(isnan(frameInWindow.size.height) || isinf(frameInWindow.size.height))
|
|
680
|
+
? 0
|
|
681
|
+
: frameInWindow.size.height;
|
|
682
|
+
CGRect sanitizedFrame = CGRectMake(x, y, w, h);
|
|
683
|
+
|
|
684
|
+
if (!CGRectIsEmpty(sanitizedFrame) && sanitizedFrame.size.width > 10 &&
|
|
685
|
+
sanitizedFrame.size.height > 10 &&
|
|
686
|
+
CGRectIntersectsRect(sanitizedFrame, window.bounds)) {
|
|
687
|
+
|
|
688
|
+
if (isCamera) {
|
|
689
|
+
[self.cameraFrames addObject:[NSValue valueWithCGRect:sanitizedFrame]];
|
|
690
|
+
RJLogDebug(@"PrivacyMask: Found camera %@ at %@",
|
|
691
|
+
NSStringFromClass([view class]),
|
|
692
|
+
NSStringFromCGRect(sanitizedFrame));
|
|
693
|
+
} else if (isWebView) {
|
|
694
|
+
[self.webViewFrames addObject:[NSValue valueWithCGRect:sanitizedFrame]];
|
|
695
|
+
RJLogDebug(@"PrivacyMask: Found web view %@ at %@",
|
|
696
|
+
NSStringFromClass([view class]),
|
|
697
|
+
NSStringFromCGRect(sanitizedFrame));
|
|
698
|
+
} else if (isVideo) {
|
|
699
|
+
[self.videoFrames addObject:[NSValue valueWithCGRect:sanitizedFrame]];
|
|
700
|
+
RJLogDebug(@"PrivacyMask: Found video view %@ at %@",
|
|
701
|
+
NSStringFromClass([view class]),
|
|
702
|
+
NSStringFromCGRect(sanitizedFrame));
|
|
703
|
+
} else {
|
|
704
|
+
|
|
705
|
+
[self.textInputFrames
|
|
706
|
+
addObject:[NSValue valueWithCGRect:sanitizedFrame]];
|
|
707
|
+
RJLogDebug(@"PrivacyMask: Found %@ %@ at %@",
|
|
708
|
+
isManuallyMasked ? @"masked view" : @"text input",
|
|
709
|
+
NSStringFromClass([view class]),
|
|
710
|
+
NSStringFromCGRect(sanitizedFrame));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (UIView *subview in view.subviews) {
|
|
716
|
+
[self scanView:subview inWindow:window];
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#pragma mark - Text Input Detection (STRICT)
|
|
721
|
+
|
|
722
|
+
- (BOOL)isActualTextInput:(UIView *)view {
|
|
723
|
+
if (!view)
|
|
724
|
+
return NO;
|
|
725
|
+
|
|
726
|
+
if ([view isKindOfClass:[UITextField class]]) {
|
|
727
|
+
return YES;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if ([view isKindOfClass:[UITextView class]]) {
|
|
731
|
+
UITextView *tv = (UITextView *)view;
|
|
732
|
+
return tv.isEditable;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if ([view isKindOfClass:[UISearchBar class]]) {
|
|
736
|
+
return YES;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
NSString *className = NSStringFromClass([view class]);
|
|
740
|
+
|
|
741
|
+
static NSSet<NSString *> *rnInputClasses = nil;
|
|
742
|
+
static dispatch_once_t onceToken;
|
|
743
|
+
dispatch_once(&onceToken, ^{
|
|
744
|
+
rnInputClasses = [NSSet setWithArray:@[
|
|
745
|
+
@"RCTUITextField",
|
|
746
|
+
@"RCTBaseTextInputView",
|
|
747
|
+
@"RCTSinglelineTextInputView",
|
|
748
|
+
@"RCTMultilineTextInputView",
|
|
749
|
+
]];
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
if ([rnInputClasses containsObject:className]) {
|
|
753
|
+
return YES;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
Class currentClass = [view class];
|
|
757
|
+
while (currentClass && currentClass != [UIView class]) {
|
|
758
|
+
NSString *name = NSStringFromClass(currentClass);
|
|
759
|
+
if ([name isEqualToString:@"RCTBaseTextInputView"]) {
|
|
760
|
+
return YES;
|
|
761
|
+
}
|
|
762
|
+
currentClass = [currentClass superclass];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if ([view isFirstResponder] &&
|
|
766
|
+
[view conformsToProtocol:@protocol(UITextInput)]) {
|
|
767
|
+
|
|
768
|
+
NSString *className = NSStringFromClass([view class]);
|
|
769
|
+
if (![className containsString:@"Keyboard"] &&
|
|
770
|
+
![className containsString:@"InputView"]) {
|
|
771
|
+
return YES;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return NO;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
#pragma mark - Manual Masking Detection
|
|
779
|
+
|
|
780
|
+
- (BOOL)isManuallyMaskedView:(UIView *)view {
|
|
781
|
+
if (!view)
|
|
782
|
+
return NO;
|
|
783
|
+
|
|
784
|
+
if (view.tag == 98765) {
|
|
785
|
+
return YES;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if ([view.accessibilityHint isEqualToString:@"rejourney_occlude"]) {
|
|
789
|
+
return YES;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
NSString *nativeID = view.accessibilityIdentifier;
|
|
793
|
+
if (nativeID.length > 0) {
|
|
794
|
+
if ([self.maskedNativeIDs containsObject:nativeID]) {
|
|
795
|
+
RJLogDebug(@"PrivacyMask: Found masked nativeID in view: %@", nativeID);
|
|
796
|
+
return YES;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
for (UIView *subview in view.subviews) {
|
|
801
|
+
NSString *childNativeID = subview.accessibilityIdentifier;
|
|
802
|
+
if (childNativeID.length > 0 &&
|
|
803
|
+
[self.maskedNativeIDs containsObject:childNativeID]) {
|
|
804
|
+
RJLogDebug(@"PrivacyMask: Found masked nativeID in child: %@",
|
|
805
|
+
childNativeID);
|
|
806
|
+
return YES;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return NO;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
#pragma mark - Manual nativeID Masking
|
|
814
|
+
|
|
815
|
+
- (void)addMaskedNativeID:(NSString *)nativeID {
|
|
816
|
+
if (nativeID.length > 0) {
|
|
817
|
+
[self.maskedNativeIDs addObject:nativeID];
|
|
818
|
+
RJLogDebug(@"PrivacyMask: Added masked nativeID: %@", nativeID);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
- (void)removeMaskedNativeID:(NSString *)nativeID {
|
|
823
|
+
if (nativeID.length > 0) {
|
|
824
|
+
[self.maskedNativeIDs removeObject:nativeID];
|
|
825
|
+
RJLogDebug(@"PrivacyMask: Removed masked nativeID: %@", nativeID);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#pragma mark - Camera Detection
|
|
830
|
+
|
|
831
|
+
- (BOOL)isCameraPreview:(UIView *)view {
|
|
832
|
+
if (!view)
|
|
833
|
+
return NO;
|
|
834
|
+
|
|
835
|
+
if ([view.layer isKindOfClass:[AVCaptureVideoPreviewLayer class]]) {
|
|
836
|
+
return YES;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
for (CALayer *sublayer in view.layer.sublayers) {
|
|
840
|
+
if ([sublayer isKindOfClass:[AVCaptureVideoPreviewLayer class]]) {
|
|
841
|
+
return YES;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
NSString *className = NSStringFromClass([view class]);
|
|
846
|
+
|
|
847
|
+
static NSSet<NSString *> *cameraClasses = nil;
|
|
848
|
+
static dispatch_once_t onceToken;
|
|
849
|
+
dispatch_once(&onceToken, ^{
|
|
850
|
+
cameraClasses = [NSSet setWithArray:@[
|
|
851
|
+
@"AVCaptureVideoPreviewView",
|
|
852
|
+
@"CameraView",
|
|
853
|
+
@"CKCameraView",
|
|
854
|
+
@"RNCameraView",
|
|
855
|
+
@"VisionCameraView",
|
|
856
|
+
@"RNVisionCameraView",
|
|
857
|
+
@"ExpoCameraView",
|
|
858
|
+
]];
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
if ([cameraClasses containsObject:className]) {
|
|
862
|
+
return YES;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return NO;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
- (BOOL)isWebViewSurface:(UIView *)view {
|
|
869
|
+
if (!view)
|
|
870
|
+
return NO;
|
|
871
|
+
|
|
872
|
+
Class wkClass = NSClassFromString(@"WKWebView");
|
|
873
|
+
if (wkClass && [view isKindOfClass:wkClass]) {
|
|
874
|
+
return YES;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
NSString *className = NSStringFromClass([view class]);
|
|
878
|
+
static NSSet<NSString *> *webViewClasses = nil;
|
|
879
|
+
static dispatch_once_t onceToken;
|
|
880
|
+
dispatch_once(&onceToken, ^{
|
|
881
|
+
webViewClasses = [NSSet setWithArray:@[
|
|
882
|
+
@"WKWebView",
|
|
883
|
+
@"UIWebView",
|
|
884
|
+
@"RCTWebView",
|
|
885
|
+
@"RNCWebView",
|
|
886
|
+
@"RNCWKWebView",
|
|
887
|
+
@"RCTWKWebView",
|
|
888
|
+
@"RNWebView",
|
|
889
|
+
]];
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
return [webViewClasses containsObject:className] ||
|
|
893
|
+
[className containsString:@"WebView"];
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
- (BOOL)isVideoLayerView:(UIView *)view {
|
|
897
|
+
if (!view)
|
|
898
|
+
return NO;
|
|
899
|
+
|
|
900
|
+
if ([view.layer isKindOfClass:[AVPlayerLayer class]]) {
|
|
901
|
+
return YES;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
for (CALayer *sublayer in view.layer.sublayers) {
|
|
905
|
+
if ([sublayer isKindOfClass:[AVPlayerLayer class]]) {
|
|
906
|
+
return YES;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
NSString *className = NSStringFromClass([view class]);
|
|
911
|
+
return ([className containsString:@"Video"] &&
|
|
912
|
+
[className containsString:@"View"]);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
#pragma mark - Cleanup
|
|
916
|
+
|
|
917
|
+
- (void)forceCleanup {
|
|
918
|
+
|
|
919
|
+
[self.scannedViews removeAllObjects];
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
@end
|