@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,591 @@
1
+ /**
2
+ * Global touch event interception.
3
+ * Ported from iOS RJTouchInterceptor.
4
+ *
5
+ * Matches iOS behavior:
6
+ * - Touch path tracking with x, y, timestamp, force
7
+ * - Coordinate normalization to density-independent pixels
8
+ * - Motion velocity tracking for scroll/swipe events
9
+ * - Coalesced touch processing for smooth gestures
10
+ */
11
+ package com.rejourney.touch
12
+
13
+ import android.content.Context
14
+ import android.os.Handler
15
+ import android.os.Looper
16
+ import android.view.MotionEvent
17
+ import android.view.View
18
+ import android.view.ViewGroup
19
+ import android.view.Window
20
+ import com.facebook.react.bridge.ReactApplicationContext
21
+ import com.rejourney.core.GestureType
22
+ import com.rejourney.core.Logger
23
+ import com.rejourney.utils.WindowUtils
24
+ import kotlin.concurrent.thread
25
+ import kotlin.math.atan2
26
+ import kotlin.math.sqrt
27
+
28
+ /**
29
+ * Touch point data matching iOS RJTouchPoint.
30
+ */
31
+ data class TouchPoint(
32
+ val x: Float, // Density-independent pixels (dp)
33
+ val y: Float, // Density-independent pixels (dp)
34
+ val timestamp: Long,
35
+ val force: Float // Pressure (0.0-1.0)
36
+ ) {
37
+ fun toMap(): Map<String, Any> = mapOf(
38
+ "x" to x,
39
+ "y" to y,
40
+ "timestamp" to timestamp,
41
+ "force" to force
42
+ )
43
+ }
44
+
45
+ /**
46
+ * Callback interface for touch interceptor events.
47
+ */
48
+ interface TouchInterceptorDelegate {
49
+ fun onTouchEvent(event: MotionEvent, gestureType: String?)
50
+ fun onGestureRecognized(gestureType: String, x: Float, y: Float, details: Map<String, Any?>)
51
+ fun onRageTap(tapCount: Int, x: Float, y: Float)
52
+
53
+ /** Called when a gesture completes with full touch path data (matching iOS). */
54
+ fun onGestureWithTouchPath(
55
+ gestureType: String,
56
+ touches: List<Map<String, Any>>,
57
+ duration: Long,
58
+ targetLabel: String?
59
+ ) {}
60
+
61
+ /** Called for motion events (scroll, swipe, pan) with velocity data (matching iOS). */
62
+ fun onMotionEvent(
63
+ type: String, // "scroll", "swipe", "pan"
64
+ t0: Long, // Start timestamp
65
+ t1: Long, // End timestamp
66
+ dx: Float, // Delta X (dp)
67
+ dy: Float, // Delta Y (dp)
68
+ v0: Float, // Initial velocity
69
+ v1: Float, // Final velocity
70
+ curve: String // "linear", "exponential_decay", "ease_out"
71
+ ) {}
72
+
73
+ /** Called when interaction starts (matching iOS touchInterceptorDidDetectInteractionStart). */
74
+ fun onInteractionStart() {}
75
+
76
+ /** Whether the SDK is currently recording a session. */
77
+ fun isCurrentlyRecording(): Boolean = true
78
+
79
+ /** Whether the keyboard is visible (used to classify keyboard taps). */
80
+ fun isKeyboardCurrentlyVisible(): Boolean = false
81
+
82
+ /** Current keyboard height in pixels. */
83
+ fun currentKeyboardHeight(): Int = 0
84
+ }
85
+
86
+ /**
87
+ * Intercepts global touch events for gesture detection and recording.
88
+ */
89
+ class TouchInterceptor private constructor(private val context: Context) {
90
+
91
+ companion object {
92
+ @Volatile
93
+ private var instance: TouchInterceptor? = null
94
+ // Store ReactApplicationContext separately for activity access
95
+ private var reactContext: ReactApplicationContext? = null
96
+
97
+ // Touch coalescing interval (matching iOS kCoalesceInterval = 50ms)
98
+ private const val COALESCE_INTERVAL_MS = 50L
99
+
100
+ fun getInstance(context: Context): TouchInterceptor {
101
+ // If this is a ReactApplicationContext, store it for later use
102
+ if (context is ReactApplicationContext) {
103
+ reactContext = context
104
+ }
105
+ return instance ?: synchronized(this) {
106
+ instance ?: TouchInterceptor(context.applicationContext).also { instance = it }
107
+ }
108
+ }
109
+ }
110
+
111
+ var delegate: TouchInterceptorDelegate? = null
112
+ private val gestureClassifier = GestureClassifier()
113
+ private var isEnabled: Boolean = false
114
+ private var currentWindowCallback: Window.Callback? = null
115
+ private var originalCallback: Window.Callback? = null
116
+ private val mainHandler = Handler(Looper.getMainLooper())
117
+
118
+ // Screen density for coordinate normalization (iOS uses points, Android uses pixels)
119
+ private var displayDensity: Float = 1f
120
+
121
+ private data class CoalescedTouch(
122
+ val pointerId: Int,
123
+ val x: Float,
124
+ val y: Float,
125
+ val timestamp: Long,
126
+ val force: Float,
127
+ val onKeyboard: Boolean
128
+ )
129
+
130
+ init {
131
+ displayDensity = context.resources.displayMetrics.density
132
+ }
133
+
134
+ /**
135
+ * Enable global touch tracking by installing a Window.Callback wrapper.
136
+ * Matches iOS enableGlobalTracking behavior.
137
+ */
138
+ fun enableGlobalTracking() {
139
+ if (isEnabled) return
140
+
141
+ try {
142
+ val activity = reactContext?.currentActivity
143
+ if (activity == null) {
144
+ Logger.debug("TouchInterceptor: No current activity, cannot enable tracking")
145
+ return
146
+ }
147
+
148
+ val window = activity.window
149
+ originalCallback = window.callback
150
+
151
+ currentWindowCallback = object : Window.Callback by originalCallback!! {
152
+ override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
153
+ event?.let { handleTouchEvent(it) }
154
+ return originalCallback?.dispatchTouchEvent(event) ?: false
155
+ }
156
+ }
157
+
158
+ window.callback = currentWindowCallback
159
+ isEnabled = true
160
+ Logger.debug("TouchInterceptor: Global tracking enabled")
161
+ } catch (e: Exception) {
162
+ Logger.error("Failed to enable global touch tracking", e)
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Disable global touch tracking by restoring original Window.Callback.
168
+ * Matches iOS disableGlobalTracking behavior.
169
+ */
170
+ fun disableGlobalTracking() {
171
+ if (!isEnabled) return
172
+
173
+ try {
174
+ val activity = reactContext?.currentActivity
175
+ if (activity != null && originalCallback != null) {
176
+ activity.window.callback = originalCallback
177
+ }
178
+
179
+ currentWindowCallback = null
180
+ originalCallback = null
181
+ isEnabled = false
182
+ resetTouchState()
183
+ Logger.debug("TouchInterceptor: Global tracking disabled")
184
+ } catch (e: Exception) {
185
+ Logger.error("Failed to disable global touch tracking", e)
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Handle incoming touch events and route to appropriate handlers.
191
+ */
192
+ private fun handleTouchEvent(event: MotionEvent) {
193
+ if (!isEnabled) return
194
+
195
+ val delegate = delegate ?: return
196
+ if (!delegate.isCurrentlyRecording()) return
197
+
198
+ val windowContext = reactContext ?: context
199
+ val window = WindowUtils.keyWindow(windowContext) ?: return
200
+ val timestamp = System.currentTimeMillis()
201
+ val keyboardVisible = delegate.isKeyboardCurrentlyVisible()
202
+ val keyboardHeight = if (keyboardVisible) delegate.currentKeyboardHeight() else 0
203
+
204
+ when (event.actionMasked) {
205
+ MotionEvent.ACTION_DOWN -> {
206
+ activeTouchCount = 1
207
+ touchStartTime = timestamp
208
+ touchPaths.clear()
209
+
210
+ val pointerId = event.getPointerId(0)
211
+ val rawPoint = rawCoordinates(event, 0)
212
+ val windowPoint = windowPointFor(rawPoint, window)
213
+ val x = windowPoint.first
214
+ val y = windowPoint.second
215
+ val force = event.getPressure(0).coerceIn(0f, 1f)
216
+ gestureClassifier.maxForce = force
217
+ gestureClassifier.initialPinchDistance = 0f
218
+ gestureClassifier.initialRotationAngle = 0f
219
+
220
+ touchStartX = x
221
+ touchStartY = y
222
+ lastMotionX = x
223
+ lastMotionY = y
224
+ lastMotionTimestamp = timestamp
225
+
226
+ delegate.onInteractionStart()
227
+
228
+ val touchOnKeyboard = isTouchOnKeyboard(rawPoint.second, window, keyboardVisible, keyboardHeight)
229
+ val path = mutableListOf<TouchPoint>()
230
+ if (!touchOnKeyboard) {
231
+ path.add(TouchPoint(x, y, timestamp, force))
232
+ }
233
+ touchPaths[pointerId] = path
234
+ }
235
+
236
+ MotionEvent.ACTION_POINTER_DOWN -> {
237
+ activeTouchCount = event.pointerCount
238
+ val actionIndex = event.actionIndex
239
+ val pointerId = event.getPointerId(actionIndex)
240
+ val rawPoint = rawCoordinates(event, actionIndex)
241
+ val windowPoint = windowPointFor(rawPoint, window)
242
+ val x = windowPoint.first
243
+ val y = windowPoint.second
244
+ val force = event.getPressure(actionIndex).coerceIn(0f, 1f)
245
+ gestureClassifier.maxForce = maxOf(gestureClassifier.maxForce, force)
246
+
247
+ val touchOnKeyboard = isTouchOnKeyboard(rawPoint.second, window, keyboardVisible, keyboardHeight)
248
+ val path = mutableListOf<TouchPoint>()
249
+ if (!touchOnKeyboard) {
250
+ path.add(TouchPoint(x, y, timestamp, force))
251
+ }
252
+ touchPaths[pointerId] = path
253
+
254
+ if (activeTouchCount == 2) {
255
+ calculateInitialPinchState()
256
+ }
257
+ }
258
+
259
+ MotionEvent.ACTION_MOVE -> {
260
+ handleTouchMoved(event, timestamp, window, keyboardVisible, keyboardHeight)
261
+ }
262
+
263
+ MotionEvent.ACTION_UP -> {
264
+ val pointerId = event.getPointerId(0)
265
+ val rawPoint = rawCoordinates(event, 0)
266
+ val windowPoint = windowPointFor(rawPoint, window)
267
+ val x = windowPoint.first
268
+ val y = windowPoint.second
269
+ val force = event.getPressure(0).coerceIn(0f, 1f)
270
+ val touchOnKeyboard = isTouchOnKeyboard(rawPoint.second, window, keyboardVisible, keyboardHeight)
271
+ handleTouchEnded(pointerId, x, y, rawPoint, timestamp, force, touchOnKeyboard, window)
272
+ }
273
+
274
+ MotionEvent.ACTION_POINTER_UP -> {
275
+ handlePointerUp(event, timestamp, window, keyboardVisible, keyboardHeight)
276
+ }
277
+
278
+ MotionEvent.ACTION_CANCEL -> {
279
+ resetTouchState()
280
+ gestureClassifier.resetState()
281
+ }
282
+ }
283
+
284
+ delegate.onTouchEvent(event, null)
285
+ }
286
+
287
+ // Touch path tracking (matching iOS touchPaths)
288
+ private val touchPaths = mutableMapOf<Int, MutableList<TouchPoint>>()
289
+ private var touchStartTime: Long = 0
290
+ private var touchStartX: Float = 0f
291
+ private var touchStartY: Float = 0f
292
+ private var activeTouchCount: Int = 0
293
+
294
+ // Motion velocity tracking (matching iOS)
295
+ private var lastMotionX: Float = 0f
296
+ private var lastMotionY: Float = 0f
297
+ private var lastMotionTimestamp: Long = 0
298
+ private var motionVelocityX: Float = 0f
299
+ private var motionVelocityY: Float = 0f
300
+
301
+ // Touch coalescing (matching iOS coalescedTouches)
302
+ private val coalescedTouches = mutableListOf<CoalescedTouch>()
303
+ private var lastCoalescedProcessTime: Long = 0
304
+
305
+ private fun handleTouchMoved(
306
+ event: MotionEvent,
307
+ timestamp: Long,
308
+ window: Window,
309
+ keyboardVisible: Boolean,
310
+ keyboardHeight: Int
311
+ ) {
312
+ for (i in 0 until event.pointerCount) {
313
+ val pointerId = event.getPointerId(i)
314
+ val rawPoint = rawCoordinates(event, i)
315
+ val windowPoint = windowPointFor(rawPoint, window)
316
+ val x = windowPoint.first
317
+ val y = windowPoint.second
318
+ val force = event.getPressure(i).coerceIn(0f, 1f)
319
+
320
+ gestureClassifier.maxForce = maxOf(gestureClassifier.maxForce, force)
321
+ val touchOnKeyboard = isTouchOnKeyboard(rawPoint.second, window, keyboardVisible, keyboardHeight)
322
+
323
+ coalescedTouches.add(CoalescedTouch(pointerId, x, y, timestamp, force, touchOnKeyboard))
324
+ }
325
+
326
+ if (timestamp - lastCoalescedProcessTime >= COALESCE_INTERVAL_MS) {
327
+ processCoalescedTouches()
328
+ lastCoalescedProcessTime = timestamp
329
+ }
330
+ }
331
+
332
+ private fun processCoalescedTouches() {
333
+ if (coalescedTouches.isEmpty()) return
334
+
335
+ val latestTouches = mutableMapOf<Int, CoalescedTouch>()
336
+ for (touch in coalescedTouches) {
337
+ latestTouches[touch.pointerId] = touch
338
+ }
339
+
340
+ for (touch in latestTouches.values) {
341
+ if (touch.onKeyboard) continue
342
+
343
+ val path = touchPaths.getOrPut(touch.pointerId) { mutableListOf() }
344
+ val point = TouchPoint(touch.x, touch.y, touch.timestamp, touch.force)
345
+ path.add(point)
346
+
347
+ if (lastMotionTimestamp > 0) {
348
+ val dt = (touch.timestamp - lastMotionTimestamp) / 1000f
349
+ if (dt > 0 && dt < 1f) {
350
+ motionVelocityX = (touch.x - lastMotionX) / dt
351
+ motionVelocityY = (touch.y - lastMotionY) / dt
352
+ }
353
+ }
354
+ lastMotionX = touch.x
355
+ lastMotionY = touch.y
356
+ lastMotionTimestamp = touch.timestamp
357
+ }
358
+
359
+ coalescedTouches.clear()
360
+ }
361
+
362
+ private fun handleTouchEnded(
363
+ pointerId: Int,
364
+ x: Float,
365
+ y: Float,
366
+ rawPoint: Pair<Float, Float>,
367
+ timestamp: Long,
368
+ force: Float,
369
+ touchOnKeyboard: Boolean,
370
+ window: Window
371
+ ) {
372
+ processCoalescedTouches()
373
+
374
+ if (!touchOnKeyboard) {
375
+ touchPaths[pointerId]?.add(TouchPoint(x, y, timestamp, force))
376
+ }
377
+
378
+ activeTouchCount = maxOf(0, activeTouchCount - 1)
379
+
380
+ if (activeTouchCount == 0) {
381
+ if (touchOnKeyboard) {
382
+ delegate?.onGestureWithTouchPath(GestureType.KEYBOARD_TAP, emptyList(), 0, null)
383
+ } else {
384
+ finalizeGesture(timestamp, x, y, rawPoint, window)
385
+ }
386
+ resetTouchState()
387
+ }
388
+ }
389
+
390
+ private fun handlePointerUp(
391
+ event: MotionEvent,
392
+ timestamp: Long,
393
+ window: Window,
394
+ keyboardVisible: Boolean,
395
+ keyboardHeight: Int
396
+ ) {
397
+ processCoalescedTouches()
398
+
399
+ val actionIndex = event.actionIndex
400
+ val pointerId = event.getPointerId(actionIndex)
401
+ val rawPoint = rawCoordinates(event, actionIndex)
402
+ val windowPoint = windowPointFor(rawPoint, window)
403
+ val x = windowPoint.first
404
+ val y = windowPoint.second
405
+ val force = event.getPressure(actionIndex).coerceIn(0f, 1f)
406
+ gestureClassifier.maxForce = maxOf(gestureClassifier.maxForce, force)
407
+
408
+ val touchOnKeyboard = isTouchOnKeyboard(rawPoint.second, window, keyboardVisible, keyboardHeight)
409
+ if (!touchOnKeyboard) {
410
+ touchPaths[pointerId]?.add(TouchPoint(x, y, timestamp, force))
411
+ }
412
+
413
+ activeTouchCount = maxOf(0, activeTouchCount - 1)
414
+ }
415
+
416
+ private fun finalizeGesture(
417
+ timestamp: Long,
418
+ endX: Float,
419
+ endY: Float,
420
+ rawPoint: Pair<Float, Float>,
421
+ window: Window
422
+ ) {
423
+ val duration = maxOf(0, timestamp - touchStartTime)
424
+ val touchPathsCopy = touchPaths.mapValues { entry -> entry.value.map { it.toMap() } }
425
+ val touchCount = touchPathsCopy.size
426
+ val targetLabel = findTargetLabel(window, rawPoint)
427
+ val delegate = delegate ?: return
428
+
429
+ thread {
430
+ val allTouches = mutableListOf<Map<String, Any>>()
431
+ for (path in touchPathsCopy.values) {
432
+ allTouches.addAll(path)
433
+ }
434
+
435
+ val gestureType = try {
436
+ gestureClassifier.classifyMultiTouchPaths(touchPathsCopy, duration, touchCount)
437
+ } catch (e: Exception) {
438
+ Logger.warning("Gesture classification failed: ${e.message}")
439
+ GestureType.TAP
440
+ }
441
+
442
+ mainHandler.post {
443
+ delegate.onGestureWithTouchPath(gestureType, allTouches, duration, targetLabel)
444
+ emitMotionEventIfNeeded(gestureType, duration, endX, endY)
445
+ }
446
+ }
447
+
448
+ lastMotionX = 0f
449
+ lastMotionY = 0f
450
+ lastMotionTimestamp = 0
451
+ motionVelocityX = 0f
452
+ motionVelocityY = 0f
453
+ }
454
+
455
+ private fun emitMotionEventIfNeeded(gestureType: String, duration: Long, endX: Float, endY: Float) {
456
+ // Only emit for scroll/swipe/pan gestures (matching iOS)
457
+ if (!gestureType.startsWith("scroll") &&
458
+ !gestureType.startsWith("swipe") &&
459
+ !gestureType.startsWith("pan")) {
460
+ return
461
+ }
462
+
463
+ val motionType = when {
464
+ gestureType.startsWith("scroll") -> "scroll"
465
+ gestureType.startsWith("swipe") -> "swipe"
466
+ else -> "pan"
467
+ }
468
+
469
+ val dx = endX - touchStartX
470
+ val dy = endY - touchStartY
471
+ val velocity = kotlin.math.sqrt(motionVelocityX * motionVelocityX + motionVelocityY * motionVelocityY)
472
+
473
+ // Determine curve type (matching iOS)
474
+ val curve = when (motionType) {
475
+ "scroll" -> "exponential_decay"
476
+ "swipe" -> "ease_out"
477
+ else -> "linear"
478
+ }
479
+
480
+ delegate?.onMotionEvent(
481
+ type = motionType,
482
+ t0 = touchStartTime,
483
+ t1 = touchStartTime + duration,
484
+ dx = dx,
485
+ dy = dy,
486
+ v0 = velocity,
487
+ v1 = 0f,
488
+ curve = curve
489
+ )
490
+ }
491
+
492
+ private fun resetTouchState() {
493
+ touchPaths.clear()
494
+ coalescedTouches.clear()
495
+ activeTouchCount = 0
496
+ lastMotionX = 0f
497
+ lastMotionY = 0f
498
+ lastMotionTimestamp = 0
499
+ motionVelocityX = 0f
500
+ motionVelocityY = 0f
501
+ lastCoalescedProcessTime = 0
502
+ }
503
+
504
+ private fun rawCoordinates(event: MotionEvent, index: Int): Pair<Float, Float> {
505
+ val offsetX = event.rawX - event.getX(0)
506
+ val offsetY = event.rawY - event.getY(0)
507
+ val rawX = event.getX(index) + offsetX
508
+ val rawY = event.getY(index) + offsetY
509
+ return rawX to rawY
510
+ }
511
+
512
+ private fun windowPointFor(rawPoint: Pair<Float, Float>, window: Window): Pair<Float, Float> {
513
+ val rootView = window.decorView?.rootView ?: window.decorView
514
+ val location = IntArray(2)
515
+ if (rootView != null) {
516
+ rootView.getLocationOnScreen(location)
517
+ }
518
+ val density = if (displayDensity.isFinite() && displayDensity > 0f) {
519
+ displayDensity
520
+ } else {
521
+ 1f
522
+ }
523
+ val x = (rawPoint.first - location[0]) / density
524
+ val y = (rawPoint.second - location[1]) / density
525
+ return x to y
526
+ }
527
+
528
+ private fun isTouchOnKeyboard(
529
+ rawY: Float,
530
+ window: Window,
531
+ keyboardVisible: Boolean,
532
+ keyboardHeight: Int
533
+ ): Boolean {
534
+ if (!keyboardVisible || keyboardHeight <= 0) return false
535
+
536
+ val rootView = window.decorView?.rootView ?: return false
537
+ if (rootView.height <= 0) return false
538
+
539
+ val location = IntArray(2)
540
+ rootView.getLocationOnScreen(location)
541
+ val windowBottom = location[1] + rootView.height
542
+ val keyboardTop = windowBottom - keyboardHeight
543
+ return rawY >= keyboardTop
544
+ }
545
+
546
+ private fun findTargetLabel(window: Window, rawPoint: Pair<Float, Float>): String? {
547
+ val rootView = window.decorView?.rootView ?: return null
548
+ val targetView = findViewAt(rootView, rawPoint.first, rawPoint.second)
549
+ return WindowUtils.accessibilityLabelForView(targetView)
550
+ }
551
+
552
+ private fun findViewAt(view: View, rawX: Float, rawY: Float): View? {
553
+ if (view.visibility != View.VISIBLE) return null
554
+
555
+ val location = IntArray(2)
556
+ view.getLocationOnScreen(location)
557
+ val left = location[0]
558
+ val top = location[1]
559
+ val right = left + view.width
560
+ val bottom = top + view.height
561
+
562
+ if (rawX < left || rawX > right || rawY < top || rawY > bottom) return null
563
+
564
+ if (view is ViewGroup) {
565
+ for (i in view.childCount - 1 downTo 0) {
566
+ val child = view.getChildAt(i)
567
+ val candidate = findViewAt(child, rawX, rawY)
568
+ if (candidate != null) {
569
+ return candidate
570
+ }
571
+ }
572
+ }
573
+
574
+ return view
575
+ }
576
+
577
+ private fun calculateInitialPinchState() {
578
+ val touchIds = touchPaths.keys.toList()
579
+ if (touchIds.size < 2) return
580
+
581
+ val path1 = touchPaths[touchIds[0]]
582
+ val path2 = touchPaths[touchIds[1]]
583
+ val point1 = path1?.lastOrNull() ?: return
584
+ val point2 = path2?.lastOrNull() ?: return
585
+
586
+ val dx = point1.x - point2.x
587
+ val dy = point1.y - point2.y
588
+ gestureClassifier.initialPinchDistance = sqrt(dx * dx + dy * dy)
589
+ gestureClassifier.initialRotationAngle = atan2(dy, dx)
590
+ }
591
+ }