diff --git a/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/ReactNativeModule.kt b/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/ReactNativeModule.kt index 758d83978..10037a91f 100644 --- a/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/ReactNativeModule.kt +++ b/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/ReactNativeModule.kt @@ -10,18 +10,10 @@ import com.openreplay.tracker.models.OROptions class ReactNativeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - // private val context = reactContext.acti override fun getName(): String { return NAME } - // Example method - // See https://reactnative.dev/docs/native-modules-android - @ReactMethod - fun multiply(a: Double, b: Double, promise: Promise) { - promise.resolve(a * b * 2) - } - companion object { const val NAME = "ORTrackerConnector" } @@ -33,14 +25,13 @@ class ReactNativeModule(reactContext: ReactApplicationContext) : val logs: Boolean = true, val screen: Boolean = true, val debugLogs: Boolean = false, - val wifiOnly: Boolean = true // assuming you want this as well + val wifiOnly: Boolean = true ) private fun getBooleanOrDefault(map: ReadableMap, key: String, default: Boolean): Boolean { return if (map.hasKey(key)) map.getBoolean(key) else default } - // optionsMap: ReadableMap?, @ReactMethod fun startSession( projectKey: String, @@ -97,8 +88,8 @@ class ReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod fun getSessionID(promise: Promise) { try { - val sessionId = OpenReplay.getSessionID() ?: "" - promise.resolve(sessionId) // Resolve the promise with the session ID + val sessionId = OpenReplay.getSessionID() + promise.resolve(sessionId) } catch (e: Exception) { promise.reject("GET_SESSION_ID_ERROR", "Failed to retrieve session ID", e) } @@ -111,8 +102,9 @@ class ReactNativeModule(reactContext: ReactApplicationContext) : requestJSON: String, responseJSON: String, status: Int, - duration: ULong + duration: Double ) { - OpenReplay.networkRequest(url, method, requestJSON, responseJSON, status, duration) + val durationULong = duration.toLong().toULong() + OpenReplay.networkRequest(url, method, requestJSON, responseJSON, status, durationULong) } } diff --git a/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/RntrackerTouchManager.kt b/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/RntrackerTouchManager.kt index bdfcf3d7a..b167fdf2c 100644 --- a/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/RntrackerTouchManager.kt +++ b/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/RntrackerTouchManager.kt @@ -1,13 +1,12 @@ package com.openreplay.reactnative -import android.annotation.SuppressLint import android.content.Context import android.graphics.PointF +import android.util.Log import android.view.MotionEvent import android.view.View +import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.Toast -import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.openreplay.tracker.listeners.Analytics @@ -15,151 +14,16 @@ import com.openreplay.tracker.listeners.SwipeDirection import kotlin.math.abs import kotlin.math.sqrt - -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.GestureDetector -import com.facebook.react.ReactRootView - -//class RnTrackerTouchManager : ViewGroupManager() { -// override fun getName(): String = "RnTrackerTouchView" -// -// override fun createViewInstance(reactContext: ThemedReactContext): TouchableFrameLayout { -// return TouchableFrameLayout(reactContext) -// } -//} -// -//class TouchableFrameLayout(context: Context) : FrameLayout(context) { -// private var gestureDetector: GestureDetector -// private var handler = Handler(Looper.getMainLooper()) -// private var isScrolling = false -// private var lastX: Float = 0f -// private var lastY: Float = 0f -// private var swipeDirection: SwipeDirection = SwipeDirection.UNDEFINED -// -// init { -// gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { -// override fun onSingleTapUp(e: MotionEvent): Boolean { -// Analytics.sendClick(e) -// return true -// } -// -// override fun onDown(e: MotionEvent): Boolean = true -// -// override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { -// if (!isScrolling) { -// isScrolling = true -// } -// -// swipeDirection = SwipeDirection.fromDistances(distanceX, distanceY) -// lastX = e2.x -// lastY = e2.y -// -// handler.removeCallbacksAndMessages(null) -// handler.postDelayed({ -// if (isScrolling) { -// isScrolling = false -// Analytics.sendSwipe(swipeDirection, lastX, lastY) -// } -// }, 200) -// return true -// } -// }) -// -// setOnTouchListener { _, event -> -// Log.d("TouchEvent", "Event: ${event.actionMasked}, X: ${event.x}, Y: ${event.y}") -// gestureDetector.onTouchEvent(event) -// this.performClick() -// } -// } -//} - - class RnTrackerTouchManager : ViewGroupManager() { + override fun getName(): String = "RnTrackerTouchView" override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout { - return ReactRootView(reactContext).apply { -// layoutParams = FrameLayout.LayoutParams( -// FrameLayout.LayoutParams.MATCH_PARENT, -// FrameLayout.LayoutParams.MATCH_PARENT -// ) -// isClickable = true -// val touchStart = PointF() -// setOnTouchListener { view, event -> -// when (event.action) { -// MotionEvent.ACTION_DOWN -> { -// touchStart.set(event.x, event.y) -// true -// } -// -// MotionEvent.ACTION_UP -> { -// val deltaX = event.x - touchStart.x -// val deltaY = event.y - touchStart.y -// val distance = sqrt(deltaX * deltaX + deltaY * deltaY) -// -// if (distance > 10) { -// val direction = if (abs(deltaX) > abs(deltaY)) { -// if (deltaX > 0) "RIGHT" else "LEFT" -// } else { -// if (deltaY > 0) "DOWN" else "UP" -// } -// Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y) -// } else { -// Analytics.sendClick(event) -// view.performClick() // Perform click for accessibility -// } -// true -// } -// -// else -> false -// } -// } - } + return RnTrackerRootLayout(reactContext) } override fun addView(parent: FrameLayout, child: View, index: Int) { - child.isClickable = true - child.isFocusable = true -// child.layoutParams = FrameLayout.LayoutParams( -// FrameLayout.LayoutParams.MATCH_PARENT, -// FrameLayout.LayoutParams.MATCH_PARENT -// ) - val touchStart = PointF() - child.setOnTouchListener( - View.OnTouchListener { view, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - view.performClick() - Analytics.sendClick(event) - true - } - - MotionEvent.ACTION_UP -> { - val deltaX = event.x - touchStart.x - val deltaY = event.y - touchStart.y - val distance = sqrt(deltaX * deltaX + deltaY * deltaY) - - if (distance > 10) { - val direction = if (abs(deltaX) > abs(deltaY)) { - if (deltaX > 0) "RIGHT" else "LEFT" - } else { - if (deltaY > 0) "DOWN" else "UP" - } - Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y) - } else { - Analytics.sendClick(event) - view.performClick() // Perform click for accessibility - } - true - } - - else -> false - } - } - ) - parent.addView(child) + parent.addView(child, index) } override fun getChildCount(parent: FrameLayout): Int = parent.childCount @@ -175,63 +39,79 @@ class RnTrackerTouchManager : ViewGroupManager() { } } -//class RnTrackerTouchManager : ViewGroupManager() { -// override fun getName(): String = "RnTrackerTouchView" -// -// override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout { -// return FrameLayout(reactContext).apply { -// layoutParams = FrameLayout.LayoutParams( -// FrameLayout.LayoutParams.MATCH_PARENT, -// FrameLayout.LayoutParams.MATCH_PARENT -// ) -// isClickable = true -// val touchStart = PointF() -// setOnTouchListener { view, event -> -// when (event.action) { -// MotionEvent.ACTION_DOWN -> { -// touchStart.set(event.x, event.y) -// view.performClick() -// } -// -// MotionEvent.ACTION_UP -> { -// val deltaX = event.x - touchStart.x -// val deltaY = event.y - touchStart.y -// val distance = sqrt(deltaX * deltaX + deltaY * deltaY) -// -// if (distance > 10) { -// val direction = if (abs(deltaX) > abs(deltaY)) { -// if (deltaX > 0) "RIGHT" else "LEFT" -// } else { -// if (deltaY > 0) "DOWN" else "UP" -// } -// Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y) -// view.performClick() -// } else { -// Analytics.sendClick(event) -// view.performClick() -// } -// true -// } -// -// else -> false -// } -// } -// } -// } -// -// override fun addView(parent: FrameLayout, child: View, index: Int) { -// parent.addView(child, index) -// } -// -// override fun getChildCount(parent: FrameLayout): Int = parent.childCount -// -// override fun getChildAt(parent: FrameLayout, index: Int): View = parent.getChildAt(index) -// -// override fun removeViewAt(parent: FrameLayout, index: Int) { -// parent.removeViewAt(index) -// } -// -// override fun removeAllViews(parent: FrameLayout) { -// parent.removeAllViews() -// } -//} +class RnTrackerRootLayout(context: Context) : FrameLayout(context) { + private val touchStart = PointF() + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + // Record the starting point for swipe/tap differentiation + touchStart.x = ev.x + touchStart.y = ev.y + Log.d("RnTrackerRootLayout", "ACTION_DOWN at global: (${ev.rawX}, ${ev.rawY})") + } + MotionEvent.ACTION_UP -> { + val deltaX = ev.x - touchStart.x + val deltaY = ev.y - touchStart.y + val distance = sqrt(deltaX * deltaX + deltaY * deltaY) + + // Find the exact view that was tapped + val tappedView = findViewAt(this, ev.x.toInt(), ev.y.toInt()) + + if (distance > 10) { + // Consider this a swipe + val direction = if (abs(deltaX) > abs(deltaY)) { + if (deltaX > 0) "RIGHT" else "LEFT" + } else { + if (deltaY > 0) "DOWN" else "UP" + } + Log.d("RnTrackerRootLayout", "Swipe detected: $direction") + Analytics.sendSwipe(SwipeDirection.valueOf(direction), ev.rawX, ev.rawY) + } else { + // Consider this a tap + val label = tappedView?.contentDescription?.toString() ?: "Button" + Log.d("RnTrackerRootLayout", "Tap detected on $tappedView with label: $label") + + val currentTime = android.os.SystemClock.uptimeMillis() + val syntheticEvent = MotionEvent.obtain( + currentTime, + currentTime, + MotionEvent.ACTION_UP, + ev.rawX, + ev.rawY, + 0 + ) + + Analytics.sendClick(syntheticEvent, label) + syntheticEvent.recycle() + + // Perform the click on the tapped view + tappedView?.performClick() + } + } + } + // Call super to ensure normal behavior (scrolling, clicks, etc.) is not disturbed + return super.dispatchTouchEvent(ev) + } + + private fun findViewAt(parent: ViewGroup, x: Int, y: Int): View? { + for (i in parent.childCount - 1 downTo 0) { + val child = parent.getChildAt(i) + if (isPointInsideView(x, y, child)) { + if (child is ViewGroup) { + val childX = x - child.left + val childY = y - child.top + val result = findViewAt(child, childX, childY) + return result ?: child + } else { + return child + } + } + } + return null + } + + private fun isPointInsideView(x: Int, y: Int, view: View): Boolean { + return x >= view.left && x <= view.right && y >= view.top && y <= view.bottom + } +} diff --git a/tracker/tracker-reactnative/app.plugin.js b/tracker/tracker-reactnative/app.plugin.js new file mode 100644 index 000000000..695826dea --- /dev/null +++ b/tracker/tracker-reactnative/app.plugin.js @@ -0,0 +1,27 @@ +// module.exports = function (config) { +// // Modify the config as needed +// return config; +// }; + +const { withAppBuildGradle, withMainApplication } = require('@expo/config-plugins'); + +function addPackageToMainApplication(src) { + console.log('Adding OpenReplay package to MainApplication.java', src); + // Insert `packages.add(new ReactNativePackage());` before return packages; + if (src.includes('packages.add(new ReactNativePackage())')) { + return src; + } + return src.replace( + 'return packages;', + `packages.add(new com.openreplay.reactnative.ReactNativePackage());\n return packages;` + ); +} + +module.exports = function configPlugin(config) { + return withMainApplication(config, (config) => { + if (config.modResults.contents) { + config.modResults.contents = addPackageToMainApplication(config.modResults.contents); + } + return config; + }); +}; diff --git a/tracker/tracker-reactnative/package.json b/tracker/tracker-reactnative/package.json index bf1dc3901..b0adcd2e4 100644 --- a/tracker/tracker-reactnative/package.json +++ b/tracker/tracker-reactnative/package.json @@ -156,5 +156,8 @@ } ] ] + }, + "dependencies": { + "@expo/config-plugins": "^9.0.12" } }