@rejourneyco/react-native 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. package/src/types/index.ts +739 -0
@@ -0,0 +1,772 @@
1
+ //
2
+ // RJTouchInterceptor.m
3
+ // Rejourney
4
+ //
5
+ // Global touch event interception implementation.
6
+ //
7
+ // Licensed under the Apache License, Version 2.0 (the "License");
8
+ // you may not use this file except in compliance with the License.
9
+ // You may obtain a copy of the License at
10
+ //
11
+ // http://www.apache.org/licenses/LICENSE-2.0
12
+ //
13
+ // Unless required by applicable law or agreed to in writing, software
14
+ // distributed under the License is distributed on an "AS IS" BASIS,
15
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ // See the License for the specific language governing permissions and
17
+ // limitations under the License.
18
+ //
19
+ // Copyright (c) 2026 Rejourney
20
+ //
21
+
22
+ #import "RJTouchInterceptor.h"
23
+ #import "../Capture/RJMotionEvent.h"
24
+ #import "../Core/RJLogger.h"
25
+ #import "../Utils/RJWindowUtils.h"
26
+ #import "RJGestureClassifier.h"
27
+ #import <objc/runtime.h>
28
+
29
+ #pragma mark - Static Variables
30
+
31
+ static void (*_originalSendEvent)(id, SEL, UIEvent *);
32
+
33
+ static BOOL _swizzlingPerformed = NO;
34
+
35
+ #pragma mark - Swizzled Method
36
+
37
+ static void RJ_swizzled_sendEvent(id self, SEL _cmd, UIEvent *event) {
38
+ @try {
39
+ if (_originalSendEvent) {
40
+ _originalSendEvent(self, _cmd, event);
41
+ }
42
+ } @catch (NSException *exception) {
43
+ RJLogError(@"Original sendEvent failed: %@", exception);
44
+ }
45
+
46
+ @
47
+ try {
48
+ if (event && event.type == UIEventTypeTouches) {
49
+ RJTouchInterceptor *interceptor = [RJTouchInterceptor sharedInstance];
50
+ if (interceptor && interceptor.isTrackingEnabled) {
51
+ [interceptor handleTouchEvent:event];
52
+ } else {
53
+ // Debug: log why we're not handling touch (throttled to avoid spam)
54
+ static int swizzleSkipCount = 0;
55
+ if (++swizzleSkipCount % 500 == 1) {
56
+ RJLogInfo(@"[RJ-TOUCH-SWIZZLE] Touch not handled: interceptor=%@, "
57
+ @"isTrackingEnabled=%d (skipCount=%d)",
58
+ interceptor ? @"exists" : @"nil",
59
+ interceptor ? interceptor.isTrackingEnabled : -1,
60
+ swizzleSkipCount);
61
+ }
62
+ }
63
+ }
64
+ } @catch (NSException *exception) {
65
+ RJLogError(@"Touch handling failed: %@", exception);
66
+ }
67
+ }
68
+
69
+ #pragma mark - Private Interface
70
+
71
+ @interface RJTouchInterceptor ()
72
+
73
+ @property(nonatomic, strong) RJGestureClassifier *classifier;
74
+
75
+ @property(nonatomic, strong)
76
+ NSMutableDictionary<NSNumber *, NSMutableArray<NSDictionary *> *>
77
+ *touchPaths;
78
+
79
+ @property(nonatomic, assign) NSTimeInterval touchStartTime;
80
+
81
+ @property(nonatomic, assign) CGPoint touchStartPoint;
82
+
83
+ @property(nonatomic, assign) NSInteger activeTouchCount;
84
+
85
+ @property(nonatomic, assign) BOOL isTrackingEnabled;
86
+
87
+ @property(nonatomic, assign) CGPoint lastMotionPoint;
88
+ @property(nonatomic, assign) NSTimeInterval lastMotionTimestamp;
89
+ @property(nonatomic, assign) CGFloat motionVelocityX;
90
+ @property(nonatomic, assign) CGFloat motionVelocityY;
91
+
92
+ @property(nonatomic, assign) NSTimeInterval lastProcessedTouchTime;
93
+
94
+ @property(nonatomic, strong)
95
+ NSMutableArray<NSMutableDictionary *> *touchDictPool;
96
+ @property(nonatomic, assign) NSInteger poolSize;
97
+
98
+ @property(nonatomic, strong) NSMutableArray<NSDictionary *> *coalescedTouches;
99
+ @property(nonatomic, assign) NSTimeInterval lastCoalescedProcessTime;
100
+
101
+ @end
102
+
103
+ static const NSTimeInterval kTouchThrottleInterval = 0.016;
104
+
105
+ static const NSInteger kTouchDictPoolSize = 20;
106
+ static const NSTimeInterval kCoalesceInterval = 0.050;
107
+
108
+ #pragma mark - Implementation
109
+
110
+ @implementation RJTouchInterceptor
111
+
112
+ #pragma mark - Singleton
113
+
114
+ + (instancetype)sharedInstance {
115
+ static RJTouchInterceptor *instance = nil;
116
+ static dispatch_once_t onceToken;
117
+ dispatch_once(&onceToken, ^{
118
+ instance = [[RJTouchInterceptor alloc] init];
119
+ });
120
+ return instance;
121
+ }
122
+
123
+ #pragma mark - Initialization
124
+
125
+ - (instancetype)init {
126
+ self = [super init];
127
+ if (self) {
128
+ _classifier = [[RJGestureClassifier alloc] init];
129
+ _touchPaths = [NSMutableDictionary new];
130
+ _activeTouchCount = 0;
131
+ _isTrackingEnabled = NO;
132
+ _lastMotionPoint = CGPointZero;
133
+ _lastMotionTimestamp = 0;
134
+ _motionVelocityX = 0;
135
+ _motionVelocityY = 0;
136
+ _lastProcessedTouchTime = 0;
137
+
138
+ _touchDictPool = [NSMutableArray arrayWithCapacity:kTouchDictPoolSize];
139
+ _poolSize = 0;
140
+
141
+ _coalescedTouches = [NSMutableArray arrayWithCapacity:10];
142
+ _lastCoalescedProcessTime = 0;
143
+ }
144
+ return self;
145
+ }
146
+
147
+ #pragma mark - Setup
148
+
149
+ - (void)enableGlobalTracking {
150
+ RJLogInfo(@"[RJ-TOUCH-SETUP] enableGlobalTracking called, current "
151
+ @"isTrackingEnabled=%d",
152
+ self.isTrackingEnabled);
153
+ static dispatch_once_t onceToken;
154
+ dispatch_once(&onceToken, ^{
155
+ RJLogInfo(@"[RJ-TOUCH-SETUP] First-time setup - performing swizzle");
156
+ Class applicationClass = [UIApplication class];
157
+ SEL originalSelector = @selector(sendEvent:);
158
+ Method originalMethod =
159
+ class_getInstanceMethod(applicationClass, originalSelector);
160
+
161
+ if (originalMethod) {
162
+ _originalSendEvent = (void *)method_getImplementation(originalMethod);
163
+ method_setImplementation(originalMethod, (IMP)RJ_swizzled_sendEvent);
164
+ _swizzlingPerformed = YES;
165
+ self.isTrackingEnabled = YES;
166
+ RJLogInfo(@"[RJ-TOUCH-SETUP] Swizzle complete, isTrackingEnabled=%d",
167
+ self.isTrackingEnabled);
168
+ } else {
169
+ RJLogError(@"Failed to enable touch tracking - sendEvent: not found");
170
+ }
171
+ });
172
+ RJLogInfo(@"[RJ-TOUCH-SETUP] enableGlobalTracking finished, isTrackingEnabled=%d",
173
+ self.isTrackingEnabled);
174
+ }
175
+
176
+ #pragma mark - Touch Event Handling
177
+
178
+ - (void)handleTouchEvent:(UIEvent *)event {
179
+
180
+ if (!event || !self.isTrackingEnabled) {
181
+ static int skipCount = 0;
182
+ if (++skipCount % 100 == 1) {
183
+ RJLogInfo(@"[RJ-TOUCH] Skipping touch: event=%@, isTrackingEnabled=%d",
184
+ event ? @"exists" : @"nil", self.isTrackingEnabled);
185
+ }
186
+ return;
187
+ }
188
+
189
+ @try {
190
+
191
+ if (!self.delegate) {
192
+ RJLogInfo(@"[RJ-TOUCH] No delegate set, skipping touch");
193
+ return;
194
+ }
195
+
196
+ BOOL isRecording = NO;
197
+ @try {
198
+ isRecording = [self.delegate isCurrentlyRecording];
199
+ } @catch (NSException *e) {
200
+ RJLogInfo(@"[RJ-TOUCH] isCurrentlyRecording threw exception: %@", e);
201
+ return;
202
+ }
203
+
204
+ if (!isRecording) {
205
+ static int notRecordingCount = 0;
206
+ if (++notRecordingCount % 100 == 1) {
207
+ RJLogInfo(@"[RJ-TOUCH] Not recording, skipping touch (count=%d)",
208
+ notRecordingCount);
209
+ }
210
+ return;
211
+ }
212
+
213
+ UIWindow *keyWindow = [self getKeyWindow];
214
+ if (!keyWindow) {
215
+ RJLogInfo(@"[RJ-TOUCH] No keyWindow available, skipping touch");
216
+ return;
217
+ }
218
+
219
+ BOOL keyboardVisible = NO;
220
+ CGRect keyboardFrame = CGRectZero;
221
+ @try {
222
+ keyboardVisible = [self.delegate isKeyboardCurrentlyVisible];
223
+ keyboardFrame = [self.delegate currentKeyboardFrame];
224
+ } @catch (NSException *e) {
225
+ }
226
+
227
+ NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970] * 1000;
228
+
229
+ NSSet *allTouches = [event allTouches];
230
+ if (!allTouches)
231
+ return;
232
+
233
+ for (UITouch *touch in allTouches) {
234
+ @try {
235
+ [self processTouch:touch
236
+ inWindow:keyWindow
237
+ timestamp:timestamp
238
+ keyboardVisible:keyboardVisible
239
+ keyboardFrame:keyboardFrame];
240
+ } @catch (NSException *touchException) {
241
+ RJLogWarning(@"Individual touch processing failed: %@", touchException);
242
+ }
243
+ }
244
+ } @catch (NSException *exception) {
245
+ RJLogWarning(@"Touch event handling failed: %@", exception);
246
+ }
247
+ }
248
+
249
+ - (void)processTouch:(UITouch *)touch
250
+ inWindow:(UIWindow *)window
251
+ timestamp:(NSTimeInterval)timestamp
252
+ keyboardVisible:(BOOL)keyboardVisible
253
+ keyboardFrame:(CGRect)keyboardFrame {
254
+
255
+ if (!touch || !window)
256
+ return;
257
+
258
+ @try {
259
+ CGPoint location = [touch locationInView:window];
260
+
261
+ if (isnan(location.x) || isnan(location.y) || isinf(location.x) ||
262
+ isinf(location.y)) {
263
+ return;
264
+ }
265
+
266
+ NSNumber *touchId = @((NSUInteger)touch);
267
+ BOOL touchOnKeyboard =
268
+ keyboardVisible && CGRectContainsPoint(keyboardFrame, location);
269
+
270
+ CGFloat force = touch.force;
271
+ if (!isnan(force) && !isinf(force) && force > self.classifier.maxForce) {
272
+ self.classifier.maxForce = force;
273
+ }
274
+
275
+ switch (touch.phase) {
276
+ case UITouchPhaseBegan:
277
+ [self handleTouchBegan:touch
278
+ touchId:touchId
279
+ location:location
280
+ timestamp:timestamp
281
+ touchOnKeyboard:touchOnKeyboard];
282
+ break;
283
+
284
+ case UITouchPhaseMoved:
285
+ [self handleTouchMoved:touchId
286
+ location:location
287
+ timestamp:timestamp
288
+ force:force
289
+ touchOnKeyboard:touchOnKeyboard];
290
+ break;
291
+
292
+ case UITouchPhaseEnded:
293
+ case UITouchPhaseCancelled:
294
+ [self handleTouchEnded:touch
295
+ touchId:touchId
296
+ location:location
297
+ timestamp:timestamp
298
+ touchOnKeyboard:touchOnKeyboard
299
+ window:window];
300
+ break;
301
+
302
+ default:
303
+ break;
304
+ }
305
+ } @catch (NSException *exception) {
306
+ RJLogWarning(@"Touch processing error: %@", exception);
307
+ }
308
+ }
309
+
310
+ #pragma mark - Touch Phase Handlers
311
+
312
+ - (void)handleTouchBegan:(UITouch *)touch
313
+ touchId:(NSNumber *)touchId
314
+ location:(CGPoint)location
315
+ timestamp:(NSTimeInterval)timestamp
316
+ touchOnKeyboard:(BOOL)touchOnKeyboard {
317
+
318
+ self.activeTouchCount++;
319
+ self.classifier.maxForce = touch.force;
320
+
321
+ if (self.activeTouchCount == 1) {
322
+ self.touchStartTime = timestamp;
323
+ self.touchStartPoint = location;
324
+ [self.touchPaths removeAllObjects];
325
+
326
+ if (self.delegate &&
327
+ [self.delegate respondsToSelector:@selector
328
+ (touchInterceptorDidDetectInteractionStart)]) {
329
+ [self.delegate touchInterceptorDidDetectInteractionStart];
330
+ }
331
+ }
332
+
333
+ NSMutableArray *path = [NSMutableArray new];
334
+ if (!touchOnKeyboard) {
335
+ [path addObject:[self touchPointDict:location
336
+ timestamp:timestamp
337
+ force:touch.force]];
338
+ }
339
+ self.touchPaths[touchId] = path;
340
+
341
+ if (self.activeTouchCount == 2) {
342
+ [self calculateInitialPinchState];
343
+ }
344
+ }
345
+
346
+ - (void)handleTouchMoved:(NSNumber *)touchId
347
+ location:(CGPoint)location
348
+ timestamp:(NSTimeInterval)timestamp
349
+ force:(CGFloat)force
350
+ touchOnKeyboard:(BOOL)touchOnKeyboard {
351
+
352
+ NSDictionary *touchData = @{
353
+ @"id" : touchId,
354
+ @"x" : @(location.x),
355
+ @"y" : @(location.y),
356
+ @"t" : @(timestamp),
357
+ @"f" : @(force),
358
+ @"kbd" : @(touchOnKeyboard)
359
+ };
360
+
361
+ [self.coalescedTouches addObject:touchData];
362
+
363
+ if (timestamp - self.lastCoalescedProcessTime >= kCoalesceInterval) {
364
+ [self processCoalescedTouches];
365
+ self.lastCoalescedProcessTime = timestamp;
366
+ }
367
+ }
368
+
369
+ - (void)processCoalescedTouches {
370
+ if (self.coalescedTouches.count == 0)
371
+ return;
372
+
373
+ NSMutableDictionary<NSNumber *, NSDictionary *> *latestTouches =
374
+ [NSMutableDictionary new];
375
+
376
+ for (NSDictionary *touch in self.coalescedTouches) {
377
+ NSNumber *touchId = touch[@"id"];
378
+ latestTouches[touchId] = touch;
379
+ }
380
+
381
+ for (NSDictionary *touch in [latestTouches allValues]) {
382
+ NSNumber *touchId = touch[@"id"];
383
+ CGPoint location =
384
+ CGPointMake([touch[@"x"] floatValue], [touch[@"y"] floatValue]);
385
+ NSTimeInterval timestamp = [touch[@"t"] doubleValue];
386
+ CGFloat force = [touch[@"f"] floatValue];
387
+ BOOL touchOnKeyboard = [touch[@"kbd"] boolValue];
388
+
389
+ NSMutableArray *path = self.touchPaths[touchId];
390
+ if (path && !touchOnKeyboard) {
391
+ [path addObject:[self touchPointDict:location
392
+ timestamp:timestamp
393
+ force:force]];
394
+
395
+ if (self.lastMotionTimestamp > 0) {
396
+ NSTimeInterval dt = (timestamp - self.lastMotionTimestamp) / 1000.0;
397
+ if (dt > 0 && dt < 1.0) {
398
+ self.motionVelocityX = (location.x - self.lastMotionPoint.x) / dt;
399
+ self.motionVelocityY = (location.y - self.lastMotionPoint.y) / dt;
400
+ }
401
+ }
402
+ self.lastMotionPoint = location;
403
+ self.lastMotionTimestamp = timestamp;
404
+ }
405
+ }
406
+
407
+ [self.coalescedTouches removeAllObjects];
408
+ }
409
+
410
+ - (void)handleTouchEnded:(UITouch *)touch
411
+ touchId:(NSNumber *)touchId
412
+ location:(CGPoint)location
413
+ timestamp:(NSTimeInterval)timestamp
414
+ touchOnKeyboard:(BOOL)touchOnKeyboard
415
+ window:(UIWindow *)window {
416
+
417
+ @try {
418
+
419
+ NSMutableArray *path = self.touchPaths[touchId];
420
+ if (path && !touchOnKeyboard && touch) {
421
+ @try {
422
+ [path addObject:[self touchPointDict:location
423
+ timestamp:timestamp
424
+ force:touch.force]];
425
+ } @catch (NSException *e) {
426
+ }
427
+ }
428
+
429
+ self.activeTouchCount = MAX(0, self.activeTouchCount - 1);
430
+
431
+ if (self.activeTouchCount == 0) {
432
+ @try {
433
+ if (touchOnKeyboard) {
434
+
435
+ [self notifyGesture:RJGestureTypeKeyboardTap
436
+ touches:@[]
437
+ duration:0
438
+ targetLabel:nil];
439
+ } else {
440
+ [self finalizeGesture:timestamp location:location window:window];
441
+ }
442
+ } @catch (NSException *gestureException) {
443
+ RJLogWarning(@"Gesture finalization failed: %@", gestureException);
444
+ }
445
+
446
+ @
447
+ try {
448
+ [self.touchPaths removeAllObjects];
449
+ } @catch (NSException *e) {
450
+ }
451
+ }
452
+ } @catch (NSException *exception) {
453
+ RJLogWarning(@"Touch ended handling failed: %@", exception);
454
+ self.activeTouchCount = 0;
455
+ [self.touchPaths removeAllObjects];
456
+ }
457
+ }
458
+
459
+ #pragma mark - Gesture Finalization
460
+
461
+ - (void)finalizeGesture:(NSTimeInterval)timestamp
462
+ location:(CGPoint)location
463
+ window:(UIWindow *)window {
464
+
465
+ @try {
466
+
467
+ NSMutableDictionary<NSNumber *, NSArray *> *touchPathsCopy =
468
+ [self.touchPaths mutableCopy];
469
+ NSTimeInterval duration = MAX(0, timestamp - self.touchStartTime);
470
+ NSInteger touchCount = self.touchPaths.count;
471
+ CGPoint startPoint = self.touchStartPoint;
472
+ NSTimeInterval startTime = self.touchStartTime;
473
+ CGFloat velocityX = self.motionVelocityX;
474
+ CGFloat velocityY = self.motionVelocityY;
475
+
476
+ NSString *targetLabel = nil;
477
+ @try {
478
+ if (window) {
479
+ UIView *targetView = [window hitTest:location withEvent:nil];
480
+ targetLabel = [self findAccessibilityLabel:targetView];
481
+ }
482
+ } @catch (NSException *e) {
483
+ }
484
+
485
+ __weak typeof(self) weakSelf = self;
486
+ id<RJTouchInterceptorDelegate> delegate = self.delegate;
487
+
488
+ dispatch_async(
489
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
490
+ @try {
491
+
492
+ NSMutableArray<NSDictionary *> *allTouches = [NSMutableArray new];
493
+ @try {
494
+ for (NSArray *touchPath in [touchPathsCopy allValues]) {
495
+ if (touchPath && [touchPath isKindOfClass:[NSArray class]]) {
496
+ [allTouches addObjectsFromArray:touchPath];
497
+ }
498
+ }
499
+ } @catch (NSException *e) {
500
+ }
501
+
502
+ NSString *gestureType = RJGestureTypeTap;
503
+ @try {
504
+ __strong typeof(weakSelf) strongSelf = weakSelf;
505
+ if (strongSelf && strongSelf.classifier) {
506
+ gestureType = [strongSelf.classifier
507
+ classifyMultiTouchPaths:touchPathsCopy
508
+ duration:duration
509
+ touchCount:touchCount];
510
+ }
511
+ } @catch (NSException *e) {
512
+ RJLogWarning(@"Gesture classification failed: %@", e);
513
+ }
514
+
515
+ dispatch_async(dispatch_get_main_queue(), ^{
516
+ __strong typeof(weakSelf) strongSelf = weakSelf;
517
+ if (!strongSelf || !delegate)
518
+ return;
519
+
520
+ [strongSelf notifyGesture:gestureType ?: RJGestureTypeTap
521
+ touches:allTouches
522
+ duration:duration
523
+ targetLabel:targetLabel];
524
+
525
+ [strongSelf emitMotionEventIfNeeded:gestureType
526
+ duration:duration
527
+ location:location];
528
+ });
529
+ } @catch (NSException *exception) {
530
+ RJLogWarning(@"Async gesture finalization error: %@", exception);
531
+ }
532
+ });
533
+
534
+ self.lastMotionPoint = CGPointZero;
535
+ self.lastMotionTimestamp = 0;
536
+ self.motionVelocityX = 0;
537
+ self.motionVelocityY = 0;
538
+
539
+ } @catch (NSException *exception) {
540
+ RJLogWarning(@"Gesture finalization error: %@", exception);
541
+ }
542
+ }
543
+
544
+ - (void)emitMotionEventIfNeeded:(NSString *)gestureType
545
+ duration:(NSTimeInterval)duration
546
+ location:(CGPoint)location {
547
+
548
+ if (![gestureType hasPrefix:@"scroll"] && ![gestureType hasPrefix:@"swipe"] &&
549
+ ![gestureType hasPrefix:@"pan"]) {
550
+ return;
551
+ }
552
+
553
+ id<RJTouchInterceptorDelegate> delegate = self.delegate;
554
+ if (!delegate || ![delegate respondsToSelector:@selector
555
+ (touchInterceptorDidCaptureMotionEvent:)]) {
556
+ return;
557
+ }
558
+
559
+ @try {
560
+ RJMotionEvent *motionEvent = [[RJMotionEvent alloc] init];
561
+
562
+ if ([gestureType hasPrefix:@"scroll"]) {
563
+ motionEvent.type = RJMotionTypeScroll;
564
+ } else if ([gestureType hasPrefix:@"swipe"]) {
565
+ motionEvent.type = RJMotionTypeSwipe;
566
+ } else {
567
+ motionEvent.type = RJMotionTypePan;
568
+ }
569
+
570
+ motionEvent.t0 = self.touchStartTime;
571
+ motionEvent.t1 = self.touchStartTime + duration;
572
+
573
+ motionEvent.dx = location.x - self.touchStartPoint.x;
574
+ motionEvent.dy = location.y - self.touchStartPoint.y;
575
+
576
+ CGFloat velocity = sqrt(self.motionVelocityX * self.motionVelocityX +
577
+ self.motionVelocityY * self.motionVelocityY);
578
+ motionEvent.v0 = velocity;
579
+ motionEvent.v1 = 0;
580
+
581
+ if ([gestureType hasPrefix:@"scroll"]) {
582
+ motionEvent.curve = RJMotionCurveExponentialDecay;
583
+ } else if ([gestureType hasPrefix:@"swipe"]) {
584
+ motionEvent.curve = RJMotionCurveEaseOut;
585
+ } else {
586
+ motionEvent.curve = RJMotionCurveLinear;
587
+ }
588
+
589
+ [delegate touchInterceptorDidCaptureMotionEvent:motionEvent];
590
+
591
+ } @catch (NSException *e) {
592
+ RJLogWarning(@"Motion event emission failed: %@", e);
593
+ }
594
+ }
595
+
596
+ #pragma mark - Helpers
597
+
598
+ - (NSDictionary *)touchPointDict:(CGPoint)location
599
+ timestamp:(NSTimeInterval)timestamp
600
+ force:(CGFloat)force {
601
+
602
+ return @{
603
+ @"x" : @(location.x),
604
+ @"y" : @(location.y),
605
+ @"timestamp" : @(timestamp),
606
+ @"force" : @(force)
607
+ };
608
+ }
609
+
610
+ - (NSMutableDictionary *)getDictFromPool {
611
+ @synchronized(self.touchDictPool) {
612
+ if (self.poolSize > 0) {
613
+ self.poolSize--;
614
+ NSMutableDictionary *dict = [self.touchDictPool lastObject];
615
+ [self.touchDictPool removeLastObject];
616
+ [dict removeAllObjects];
617
+ return dict;
618
+ }
619
+ }
620
+
621
+ return [NSMutableDictionary dictionaryWithCapacity:4];
622
+ }
623
+
624
+ - (void)returnDictToPool:(NSMutableDictionary *)dict {
625
+ if (!dict)
626
+ return;
627
+ @synchronized(self.touchDictPool) {
628
+ if (self.poolSize < kTouchDictPoolSize) {
629
+ [dict removeAllObjects];
630
+ [self.touchDictPool addObject:dict];
631
+ self.poolSize++;
632
+ }
633
+ }
634
+ }
635
+
636
+ - (void)calculateInitialPinchState {
637
+ NSArray *touchIds = [self.touchPaths allKeys];
638
+ if (touchIds.count < 2)
639
+ return;
640
+
641
+ NSArray *path1 = self.touchPaths[touchIds[0]];
642
+ NSArray *path2 = self.touchPaths[touchIds[1]];
643
+
644
+ if (path1.count == 0 || path2.count == 0)
645
+ return;
646
+
647
+ NSDictionary *point1 = path1.lastObject;
648
+ NSDictionary *point2 = path2.lastObject;
649
+
650
+ CGFloat dx = [point1[@"x"] floatValue] - [point2[@"x"] floatValue];
651
+ CGFloat dy = [point1[@"y"] floatValue] - [point2[@"y"] floatValue];
652
+
653
+ self.classifier.initialPinchDistance = sqrt(dx * dx + dy * dy);
654
+ self.classifier.initialRotationAngle = atan2(dy, dx);
655
+ }
656
+
657
+ - (nullable NSString *)findAccessibilityLabel:(UIView *)view {
658
+ UIView *current = view;
659
+ while (current) {
660
+ if (current.accessibilityLabel.length > 0) {
661
+ return current.accessibilityLabel;
662
+ }
663
+ current = current.superview;
664
+ }
665
+ return nil;
666
+ }
667
+
668
+ - (BOOL)isViewInteractive:(UIView *)view {
669
+ UIView *current = view;
670
+ while (current) {
671
+
672
+ if ([current isKindOfClass:[UIControl class]] ||
673
+ [current isKindOfClass:[UIButton class]] ||
674
+ [current isKindOfClass:[UITextField class]] ||
675
+ [current isKindOfClass:[UITextView class]] ||
676
+ [current isKindOfClass:[UISwitch class]] ||
677
+ [current isKindOfClass:[UISlider class]] ||
678
+ [current isKindOfClass:[UITableViewCell class]] ||
679
+ [current isKindOfClass:[UICollectionViewCell class]]) {
680
+ return YES;
681
+ }
682
+
683
+ if (current.gestureRecognizers.count > 0) {
684
+ for (UIGestureRecognizer *gr in current.gestureRecognizers) {
685
+ if (gr.enabled &&
686
+ ([gr isKindOfClass:[UITapGestureRecognizer class]] ||
687
+ [gr isKindOfClass:[UILongPressGestureRecognizer class]])) {
688
+ return YES;
689
+ }
690
+ }
691
+ }
692
+
693
+ if ((current.accessibilityTraits & UIAccessibilityTraitButton) ||
694
+ (current.accessibilityTraits & UIAccessibilityTraitLink) ||
695
+ (current.accessibilityTraits & UIAccessibilityTraitKeyboardKey)) {
696
+ return YES;
697
+ }
698
+
699
+ current = current.superview;
700
+ }
701
+ return NO;
702
+ }
703
+
704
+ - (UIWindow *)getKeyWindow {
705
+ // Use shared helper to avoid accidentally selecting system input windows
706
+ // (UITextEffectsWindow / Keyboard windows) which can become "key".
707
+ UIWindow *window = [RJWindowUtils keyWindow];
708
+ if (window) {
709
+ return window;
710
+ }
711
+
712
+ if (@available(iOS 13.0, *)) {
713
+ // If RJWindowUtils returned nil, fall back to the first key window we can
714
+ // find.
715
+ for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
716
+ if (![scene isKindOfClass:[UIWindowScene class]]) {
717
+ continue;
718
+ }
719
+ UIWindowScene *windowScene = (UIWindowScene *)scene;
720
+ if (windowScene.activationState ==
721
+ UISceneActivationStateForegroundActive) {
722
+ for (UIWindow *w in windowScene.windows) {
723
+ if (w.isKeyWindow) {
724
+ return w;
725
+ }
726
+ }
727
+ }
728
+ }
729
+ } else {
730
+ #pragma clang diagnostic push
731
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
732
+ return [UIApplication sharedApplication].keyWindow;
733
+ #pragma clang diagnostic pop
734
+ }
735
+ return nil;
736
+ }
737
+
738
+ - (void)notifyGesture:(NSString *)gestureType
739
+ touches:(NSArray<NSDictionary *> *)touches
740
+ duration:(NSTimeInterval)duration
741
+ targetLabel:(nullable NSString *)targetLabel {
742
+
743
+ /*
744
+ RJLogInfo(@"[RJ-TOUCH] notifyGesture called: type=%@, touchCount=%lu, "
745
+ @"duration=%.2f",
746
+ gestureType, (unsigned long)touches.count, duration);
747
+ */
748
+
749
+ @try {
750
+ id<RJTouchInterceptorDelegate> delegate = self.delegate;
751
+ if (delegate &&
752
+ [delegate
753
+ respondsToSelector:@selector
754
+ (touchInterceptorDidRecognizeGesture:
755
+ touches:duration:targetLabel:)]) {
756
+ RJLogInfo(@"[RJ-TOUCH] Calling delegate touchInterceptorDidRecognizeGesture");
757
+ [delegate
758
+ touchInterceptorDidRecognizeGesture:gestureType ?: RJGestureTypeTap
759
+ touches:touches ?: @[]
760
+ duration:MAX(0, duration)
761
+ targetLabel:targetLabel];
762
+ } else {
763
+ RJLogInfo(@"[RJ-TOUCH] Delegate missing or doesn't respond to selector: "
764
+ @"delegate=%@",
765
+ delegate);
766
+ }
767
+ } @catch (NSException *exception) {
768
+ RJLogWarning(@"Gesture notification failed: %@", exception);
769
+ }
770
+ }
771
+
772
+ @end