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